@blac/react 2.0.14 → 2.0.16

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/README.md CHANGED
@@ -10,7 +10,7 @@ React bindings for BlaC — `useBloc` hook with proxy-based automatic re-render
10
10
  pnpm add @blac/react @blac/core
11
11
  ```
12
12
 
13
- Requires React 18+ (`useSyncExternalStore`).
13
+ Requires React 18+.
14
14
 
15
15
  ## Quick Start
16
16
 
@@ -54,11 +54,11 @@ const [state, bloc, ref] = useBloc(MyBloc, options?);
54
54
 
55
55
  BlaC blocs receive external data through three distinct channels:
56
56
 
57
- | Lane | Purpose | Keying | Lifetime | Example |
58
- |---|---|---|---|---|
59
- | **`args`** | Typed creation data; derives instance identity | **Yes** (structural hash or `static key`) | Once at `init()` | `userId`, `endpoint` |
60
- | **`deps`** | Non-serializable refs, callbacks, handles | **Never** | Live, per-consumer merged | `ref`, `onComplete` callback, `emblaApi` |
61
- | **events** | Values that change over time or are late-bound | N/A | Called from effects | `cubit.slidesChanged(v)` from `useEffect` |
57
+ | Lane | Purpose | Keying | Lifetime | Example |
58
+ | ---------- | ---------------------------------------------- | ----------------------------------------- | ------------------------- | ----------------------------------------- |
59
+ | **`args`** | Typed creation data; derives instance identity | **Yes** (structural hash or `static key`) | Once at `init()` | `userId`, `endpoint` |
60
+ | **`deps`** | Non-serializable refs, callbacks, handles | **Never** | Live, per-consumer merged | `ref`, `onComplete` callback, `emblaApi` |
61
+ | **events** | Values that change over time or are late-bound | N/A | Called from effects | `cubit.slidesChanged(v)` from `useEffect` |
62
62
 
63
63
  ### `args`: Typed Construction Data
64
64
 
@@ -84,40 +84,54 @@ function UserCard({ userId }: { userId: string }) {
84
84
  ```
85
85
 
86
86
  **Key properties:**
87
+
87
88
  - **Required when declared** — omitting `args` or passing the wrong shape is a type error.
88
89
  - **Drives identity** — different `args` ⇒ different instance. Same `args` ⇒ same instance (dev-warn if args mismatch on same-keyed second call).
89
90
  - **Serializable only** — non-serializable values (refs, callbacks) belong in the `deps` lane (below).
90
- - **Per-component private instances** — use with `autoInstance: true` to give each mount its own instance, disposed on unmount:
91
+ - **Per-component private instances** — pass a per-mount `instanceId` (keyed by React's `useId()`) to give each mount its own instance, disposed on unmount:
91
92
 
92
93
  ```tsx
93
- const [state, cubit] = useBloc(FormCubit, { args: options, autoInstance: true });
94
+ const instanceId = useId();
95
+ const [state, cubit] = useBloc(FormCubit, { args: options, instanceId });
94
96
  ```
95
97
 
96
98
  ### `deps`: Non-Serializable Refs and Callbacks
97
99
 
98
100
  Use `deps` to inject refs, stable callbacks, and long-lived controller handles. Unlike `args`, `deps` are:
101
+
99
102
  - **Never keying** — different refs don't fork the instance
100
103
  - **Per-consumer merged** — each component contributes its own slice
101
104
  - **Read lazily** — accessed via `this.deps.x` when needed, may be undefined
102
105
  - **Live** — can change over time
103
106
 
107
+ `deps` is **not** a `useBloc` option. A component contributes its slice from a **mount effect**, using `APPLY_DEPS` / `REMOVE_DEPS_OWNER` from `@blac/core` (marked `@internal` today; a friendlier wrapper may land later):
108
+
104
109
  ```tsx
110
+ import { useEffect, useId, useRef } from 'react';
111
+ import { APPLY_DEPS, REMOVE_DEPS_OWNER } from '@blac/core';
112
+ import { useBloc } from '@blac/react';
113
+
105
114
  const inputRef = useRef<HTMLInputElement>(null);
106
- const onComplete = useCallback(() => { /* ... */ }, []);
115
+ const ownerId = useId();
116
+ const [state, cubit] = useBloc(FileUploadCubit, { args: { endpoint } });
107
117
 
108
- const [state, cubit] = useBloc(FileUploadCubit, {
109
- args: { endpoint },
110
- deps: { inputRef, onComplete },
111
- });
118
+ useEffect(() => {
119
+ cubit[APPLY_DEPS](ownerId, { inputRef });
120
+ return () => cubit[REMOVE_DEPS_OWNER](ownerId);
121
+ }, [cubit, inputRef, ownerId]);
112
122
  ```
113
123
 
114
124
  The bloc reads them lazily and guards for absence:
115
125
 
116
126
  ```ts
117
- class FileUploadCubit extends Cubit<UploadState, { endpoint: string }, {
118
- inputRef?: RefObject<HTMLInputElement>;
119
- onComplete?: () => void;
120
- }> {
127
+ class FileUploadCubit extends Cubit<
128
+ UploadState,
129
+ { endpoint: string },
130
+ {
131
+ inputRef?: RefObject<HTMLInputElement>;
132
+ onComplete?: () => void;
133
+ }
134
+ > {
121
135
  async upload() {
122
136
  this.deps.inputRef?.current?.click?.();
123
137
  // ... perform upload ...
@@ -129,16 +143,17 @@ class FileUploadCubit extends Cubit<UploadState, { endpoint: string }, {
129
143
  **Multi-consumer merge:** when multiple components provide the same cubit with different `deps`, their keys are shallow-merged:
130
144
 
131
145
  ```tsx
132
- // Component A provides inputRef
133
- useBloc(FormCubit, { args: { formId }, deps: { inputRef } });
146
+ // Component A owns inputRef
147
+ cubitA[APPLY_DEPS](ownerIdA, { inputRef });
134
148
 
135
- // Component B provides onSubmit
136
- useBloc(FormCubit, { args: { formId }, deps: { onSubmit } });
149
+ // Component B owns onSubmit
150
+ cubitB[APPLY_DEPS](ownerIdB, { onSubmit });
137
151
 
138
- // cubit.deps === { inputRef, onSubmit } (merged from both)
152
+ // cubit.deps === { inputRef, onSubmit } (merged from both consumers)
139
153
  ```
140
154
 
141
155
  **Avoid raw callbacks** — the callback staleness gotcha. Prefer:
156
+
142
157
  1. **Don't inject callbacks — invert** (best): expose state and let React call the fresh callback in its own effect.
143
158
  2. **Stabilize at source** — wrap in `useCallback`.
144
159
  3. **As an event** — push the callback via a bloc method called from your effect.
@@ -168,7 +183,7 @@ function Carousel({ slides }: { slides: Slide[] }) {
168
183
  }
169
184
  ```
170
185
 
171
- **Convention:** *one component owns syncing any given live value.* Two components calling the same event from both their effects on the same shared instance is a design smell (and rare, because keyed `args` usually route multi-consumer cases to distinct instances).
186
+ **Convention:** _one component owns syncing any given live value._ Two components calling the same event from both their effects on the same shared instance is a design smell (and rare, because keyed `args` usually route multi-consumer cases to distinct instances).
172
187
 
173
188
  ### Tracking Modes
174
189
 
@@ -187,39 +202,34 @@ const [state] = useBloc(CounterCubit, {
187
202
  });
188
203
  ```
189
204
 
190
- **No tracking:** Re-renders on every state change.
191
-
192
- ```tsx
193
- const [state] = useBloc(MyBloc, { autoTrack: false });
194
- ```
195
-
196
205
  ### Options
197
206
 
198
- | Option | Type | Description |
199
- | -------------- | ---------------------------- | ---------------------------------------------------- |
200
- | `args` | `Args` type | Required when bloc declares `Args != void`; forbidden when `void` |
201
- | `deps` | `{ [key]: value }` | Per-consumer slice of non-serializable handles |
202
- | `autoTrack` | `boolean` | Enable proxy-based auto-tracking (default: `true`) |
203
- | `select` | `(state, bloc) => unknown[]` | Manual dependency selector (renamed from `dependencies`) |
204
- | `autoInstance` | `boolean` | Per-mount instance keyed by `useId()` instead of shared (default: `false`) |
205
- | `instanceId` | `string \| number` | Explicit identity key (overrides args-derived keying) |
206
- | `onMount` | `(bloc) => void` | Called when component mounts |
207
- | `onUnmount` | `(bloc) => void` | Called when component unmounts |
207
+ | Option | Type | Description |
208
+ | ------------ | ---------------------------- | -------------------------------------------------------------------------------- |
209
+ | `args` | `Args` type | Required when bloc declares `Args != void`; forbidden when `void` |
210
+ | `select` | `(state, bloc) => unknown[]` | Manual dependency selector (renamed from `dependencies`); disables auto-tracking |
211
+ | `instanceId` | `string \| number` | Explicit identity key; pass `useId()` for a per-mount instance |
212
+ | `onMount` | `(bloc) => void` | Called when component mounts |
213
+ | `onUnmount` | `(bloc) => void` | Called when component unmounts |
214
+
215
+ > Auto-tracking is always on when `select` is omitted — it is **not** a configurable option. `deps` and `autoInstance` are not `useBloc` options either: wire deps from a mount effect (`APPLY_DEPS` / `REMOVE_DEPS_OWNER`), and use `instanceId: useId()` for per-mount instances.
208
216
 
209
217
  ### Identity and Keying
210
218
 
211
219
  Instance identity is resolved in precedence order:
212
220
 
213
- 1. **Explicit `instanceId`** — hard override if provided
214
- 2. **`autoInstance: true`** — per-mount instance keyed by `useId()`
215
- 3. **`static key(args)` → structural hash of `args`** — default when bloc declares `Args` (if `static key` is defined on the class, it takes precedence; otherwise a stable hash of all `args`)
216
- 4. **`<BlocProvider>` context id** — inherited from parent provider
217
- 5. **`'default'`** — singleton fallback
221
+ 1. **Explicit `instanceId`** — hard override (pass `useId()` for a per-mount private instance)
222
+ 2. **`<BlocProvider>` context id** — inherited from an ancestor provider
223
+ 3. **`static key(args)` → structural hash of `args`** — default when the bloc declares `Args` (`static key` wins if defined; otherwise a stable hash of all `args`)
224
+ 4. **`'default'`** — singleton fallback
218
225
 
219
226
  Blocs declare explicit identity via a static class property:
220
227
 
221
228
  ```ts
222
- class DocumentCubit extends Cubit<DocState, { docId: string; readonly: boolean }> {
229
+ class DocumentCubit extends Cubit<
230
+ DocState,
231
+ { docId: string; readonly: boolean }
232
+ > {
223
233
  static key = (args) => args.docId;
224
234
  // Identity is docId; readonly config rides along but doesn't fork instances
225
235
  }
@@ -227,14 +237,15 @@ class DocumentCubit extends Cubit<DocState, { docId: string; readonly: boolean }
227
237
 
228
238
  ### Instance Sharing and Lifecycle
229
239
 
230
- By default, all components using `useBloc(MyBloc)` with the same identity share one instance. Use `autoInstance` for per-component private instances:
240
+ By default, all components using `useBloc(MyBloc)` with the same identity share one instance. Pass a per-mount `instanceId` (via `useId()`) for per-component private instances:
231
241
 
232
242
  ```tsx
233
243
  // All users with userId=123 share one instance
234
244
  useBloc(UserCardCubit, { args: { userId: 123 } });
235
245
 
236
246
  // Each component mount gets its own instance, disposed on unmount
237
- useBloc(FormCubit, { args: options, autoInstance: true });
247
+ const instanceId = useId();
248
+ useBloc(FormCubit, { args: options, instanceId });
238
249
 
239
250
  // Explicit id (escape hatch for non-derivable identity)
240
251
  useBloc(EditorCubit, { instanceId: 'editor-1' });
@@ -242,7 +253,8 @@ useBloc(EditorCubit, { instanceId: 'editor-1' });
242
253
 
243
254
  ### Breaking Changes (v2)
244
255
 
245
- - **`dependencies` option renamed to `select`** — avoids confusion with the new `deps` (non-serializable handles) lane.
256
+ - **`dependencies` option renamed to `select`** — avoids confusion with the `deps` (non-serializable handles) lane.
257
+ - **`autoTrack`, `autoInstance`, and `deps` are no longer `useBloc` options** — auto-tracking is always on (opt out per-consumer with `select`); per-mount instances use `instanceId: useId()`; deps are wired from a mount effect via `APPLY_DEPS` / `REMOVE_DEPS_OWNER`.
246
258
  - **Zero-arg constructor + `init(args)` lifecycle** — all blocs now use `new Type()` with no constructor args. Blocs that declare `Args` receive them via `init(args)` called by the framework before the first state snapshot.
247
259
  - **`args` is required when declared, forbidden when void** — enforced by the type system; no runtime guard needed.
248
260
 
@@ -251,7 +263,8 @@ useBloc(EditorCubit, { instanceId: 'editor-1' });
251
263
  ```tsx
252
264
  import { configureBlacReact } from '@blac/react';
253
265
 
254
- configureBlacReact({ autoTrack: true });
266
+ // Configuration is currently empty; the tracking model is fixed and not configurable.
267
+ configureBlacReact({});
255
268
  ```
256
269
 
257
270
  ## Testing
@@ -1,33 +1,51 @@
1
1
  import { type ReactElement, type ReactNode } from 'react';
2
+ import type { ExtractArgs, StateContainerConstructor } from '@blac/core';
2
3
  /**
3
4
  * Props for {@link BlocProvider}.
4
5
  */
5
- export interface BlocProviderProps {
6
+ export interface BlocProviderProps<T extends StateContainerConstructor> {
6
7
  /**
7
- * Instance id that descendants' `useBloc(C)` calls will resolve to when no
8
- * explicit `instanceId` is given.
8
+ * The bloc class whose args are being provided to descendants.
9
9
  */
10
- instanceId: string | number;
10
+ bloc: T;
11
+ /**
12
+ * Args that descendant `useBloc(bloc)` calls will resolve to when no
13
+ * own `args` are given.
14
+ */
15
+ args: ExtractArgs<T>;
11
16
  children: ReactNode;
12
17
  }
13
18
  /**
14
- * Provides an `instanceId` to descendant `useBloc` calls via React context.
19
+ * Provides args to descendant `useBloc` calls for a specific bloc class via
20
+ * React context.
15
21
  *
16
- * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id
17
- * supplied here. An explicit `instanceId` on the `useBloc` call still wins.
18
- * `autoInstance` / `static isolated = true` also override the context, because
19
- * those force a per-mount auto-key.
22
+ * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the
23
+ * args supplied here. Own `args` on the `useBloc` call always win.
24
+ *
25
+ * Multiple `BlocProvider` wrappers for different bloc classes compose: each
26
+ * provider merges its entry into the inherited map, so nested providers for
27
+ * different blocs do not interfere.
20
28
  *
21
29
  * @example
22
30
  * ```tsx
23
- * <BlocProvider instanceId="customer-42">
24
- * <CustomerView />
31
+ * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>
32
+ * <UserProfile />
33
+ * </BlocProvider>
34
+ * ```
35
+ *
36
+ * @example Per-mount private instance
37
+ * ```tsx
38
+ * const id = useId();
39
+ * <BlocProvider bloc={CartBloc} args={{ _id: id }}>
40
+ * <CartWidget />
25
41
  * </BlocProvider>
26
42
  * ```
27
43
  */
28
- export declare function BlocProvider({ instanceId, children, }: BlocProviderProps): ReactElement;
44
+ export declare function BlocProvider<T extends StateContainerConstructor>({ bloc, args, children, }: BlocProviderProps<T>): ReactElement;
29
45
  /**
30
- * Reads the nearest {@link BlocProvider} instance id, or `undefined` when
31
- * called outside a provider.
46
+ * Returns the args provided by the nearest {@link BlocProvider} for the given
47
+ * bloc class, or `undefined` when called outside a matching provider.
48
+ *
49
+ * Used by `useBloc` to inherit provider args when no own `args` are given.
32
50
  */
33
- export declare function useInstanceIdFromContext(): string | undefined;
51
+ export declare function useProvidedArgs<T extends StateContainerConstructor>(BlocClass: T): ExtractArgs<T> | undefined;
package/dist/index.cjs CHANGED
@@ -4,34 +4,57 @@ 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
6
  //#region src/BlocProvider.tsx
7
- const InstanceIdContext = (0, react.createContext)(void 0);
7
+ const ProvidedArgsContext = (0, react.createContext)(/* @__PURE__ */ new Map());
8
8
  /**
9
- * Provides an `instanceId` to descendant `useBloc` calls via React context.
9
+ * Provides args to descendant `useBloc` calls for a specific bloc class via
10
+ * React context.
10
11
  *
11
- * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id
12
- * supplied here. An explicit `instanceId` on the `useBloc` call still wins.
13
- * `autoInstance` / `static isolated = true` also override the context, because
14
- * those force a per-mount auto-key.
12
+ * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the
13
+ * args supplied here. Own `args` on the `useBloc` call always win.
14
+ *
15
+ * 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.
15
18
  *
16
19
  * @example
17
20
  * ```tsx
18
- * <BlocProvider instanceId="customer-42">
19
- * <CustomerView />
21
+ * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>
22
+ * <UserProfile />
23
+ * </BlocProvider>
24
+ * ```
25
+ *
26
+ * @example Per-mount private instance
27
+ * ```tsx
28
+ * const id = useId();
29
+ * <BlocProvider bloc={CartBloc} args={{ _id: id }}>
30
+ * <CartWidget />
20
31
  * </BlocProvider>
21
32
  * ```
22
33
  */
23
- function BlocProvider({ instanceId, children }) {
24
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstanceIdContext.Provider, {
25
- value: String(instanceId),
34
+ function BlocProvider({ bloc, args, children }) {
35
+ const parentMap = (0, react.useContext)(ProvidedArgsContext);
36
+ const mergedMap = (0, react.useMemo)(() => {
37
+ const next = new Map(parentMap);
38
+ next.set(bloc, args);
39
+ return next;
40
+ }, [
41
+ parentMap,
42
+ bloc,
43
+ args
44
+ ]);
45
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidedArgsContext.Provider, {
46
+ value: mergedMap,
26
47
  children
27
48
  });
28
49
  }
29
50
  /**
30
- * Reads the nearest {@link BlocProvider} instance id, or `undefined` when
31
- * called outside a provider.
51
+ * Returns the args provided by the nearest {@link BlocProvider} for the given
52
+ * bloc class, or `undefined` when called outside a matching provider.
53
+ *
54
+ * Used by `useBloc` to inherit provider args when no own `args` are given.
32
55
  */
33
- function useInstanceIdFromContext() {
34
- return (0, react.useContext)(InstanceIdContext);
56
+ function useProvidedArgs(BlocClass) {
57
+ return (0, react.useContext)(ProvidedArgsContext).get(BlocClass);
35
58
  }
36
59
  //#endregion
37
60
  //#region src/useBloc.ts
@@ -51,12 +74,19 @@ let nextConsumerId = 0;
51
74
  *
52
75
  * Lifecycle:
53
76
  * - The bloc is acquired from the registry on mount and released on
54
- * unmount. The instance key is the explicit `options.instanceId`, then
55
- * the surrounding {@link BlocProvider} context, then the default key.
77
+ * unmount. The instance key is derived from `options.args` (own args),
78
+ * then the surrounding {@link BlocProvider} context args for this bloc,
79
+ * then the default key (no args).
56
80
  * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`
57
81
  * fires *before* the registry releases its ref, so the bloc is still
58
82
  * alive when the callback runs.
59
83
  *
84
+ * Per-mount private instance:
85
+ * ```ts
86
+ * const id = useId();
87
+ * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });
88
+ * ```
89
+ *
60
90
  * @template T - The state container constructor type (inferred from BlocClass)
61
91
  * @param BlocClass - The state container class to connect to
62
92
  * @param options - Configuration options
@@ -74,9 +104,9 @@ let nextConsumerId = 0;
74
104
  * });
75
105
  * ```
76
106
  *
77
- * @example Named instance
107
+ * @example Args-based shared instance
78
108
  * ```ts
79
- * const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });
109
+ * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });
80
110
  * ```
81
111
  */
82
112
  function useBloc(BlocClass, options) {
@@ -91,26 +121,34 @@ function useBloc(BlocClass, options) {
91
121
  onMountRef.current = options?.onMount;
92
122
  const onUnmountRef = (0, react.useRef)(options?.onUnmount);
93
123
  onUnmountRef.current = options?.onUnmount;
94
- const args = options?.args;
95
- const argsRef = (0, react.useRef)(args);
96
- argsRef.current = args;
97
- const argsKey = args === void 0 ? void 0 : JSON.stringify(args);
98
- const explicitInstanceId = options?.instanceId;
99
- const ctxInstanceId = useInstanceIdFromContext();
124
+ const ownArgs = options?.args;
125
+ const ownArgsRef = (0, react.useRef)(ownArgs);
126
+ ownArgsRef.current = ownArgs;
127
+ const ownArgsKey = ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs);
128
+ const providerArgs = useProvidedArgs(BlocClass);
129
+ const providerArgsRef = (0, react.useRef)(providerArgs);
130
+ providerArgsRef.current = providerArgs;
100
131
  const { bloc, instanceKey } = (0, react.useMemo)(() => {
101
- const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, explicitInstanceId !== void 0 ? String(explicitInstanceId) : ctxInstanceId, argsRef.current);
132
+ const effectiveArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
133
+ const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, effectiveArgs);
134
+ const refId = `useBloc@${consumerId}`;
102
135
  return {
103
- bloc: (0, _blac_core.acquire)(BlocClass, resolvedKey, `useBloc@${consumerId}`, argsRef.current),
136
+ bloc: (0, _blac_core.getRegistry)().acquire(BlocClass, resolvedKey, {
137
+ canCreate: true,
138
+ countRef: true,
139
+ refId,
140
+ args: effectiveArgs
141
+ }),
104
142
  instanceKey: resolvedKey
105
143
  };
106
144
  }, [
107
145
  BlocClass,
108
- explicitInstanceId,
109
- ctxInstanceId,
110
- argsKey
146
+ ownArgsKey,
147
+ providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
111
148
  ]);
112
149
  const [, force] = (0, react.useReducer)((x) => x + 1, 0);
113
150
  const pathRef = (0, react.useRef)((0, _dirtytalk_structural.emptyPathSet)());
151
+ const expandedInterestRef = (0, react.useRef)((0, _dirtytalk_structural.emptyPathSet)());
114
152
  const lastSelectionRef = (0, react.useRef)(null);
115
153
  (0, react.useEffect)(() => {
116
154
  const channel = bloc.channel;
@@ -126,7 +164,7 @@ function useBloc(BlocClass, options) {
126
164
  lastSelectionRef.current = next;
127
165
  force();
128
166
  });
129
- const unsub = channel.subscribe(() => pathRef.current, () => force());
167
+ const unsub = channel.subscribe(() => expandedInterestRef.current, () => force());
130
168
  bloc.registerConsumerPaths(consumerId, pathRef.current);
131
169
  return () => {
132
170
  unsub();
@@ -137,7 +175,8 @@ function useBloc(BlocClass, options) {
137
175
  onMountRef.current?.(bloc);
138
176
  return () => {
139
177
  onUnmountRef.current?.(bloc);
140
- (0, _blac_core.release)(BlocClass, instanceKey, false, `useBloc@${consumerId}`);
178
+ const refId = `useBloc@${consumerId}`;
179
+ (0, _blac_core.getRegistry)().release(BlocClass, instanceKey, false, refId);
141
180
  };
142
181
  }, [bloc, instanceKey]);
143
182
  const rawState = bloc.state;
@@ -152,7 +191,10 @@ function useBloc(BlocClass, options) {
152
191
  }
153
192
  (0, react.useLayoutEffect)(() => {
154
193
  if (selectRef.current !== void 0) return;
155
- bloc.registerConsumerPaths(consumerId, pathRef.current);
194
+ const container = bloc;
195
+ const paths = pathRef.current;
196
+ container.registerConsumerPaths(consumerId, paths);
197
+ expandedInterestRef.current = expandWithAncestors(paths, container.interner);
156
198
  });
157
199
  return [
158
200
  state,
@@ -166,6 +208,37 @@ const shallowArrayEqual = (a, b) => {
166
208
  for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
167
209
  return true;
168
210
  };
211
+ /**
212
+ * Expand a PathSet to include all ancestor paths of every tracked path.
213
+ *
214
+ * The auto-tracker records leaf paths (e.g. `'items.length'`), but
215
+ * `StructuralContainer.patch` marks parent paths (e.g. `'items'`) since it
216
+ * only walks the patch shape — not individual array elements or nested
217
+ * descendants. Without ancestor expansion, a channel subscriber with interest
218
+ * `{'items.length'}` would miss a `patch`-triggered `dirty={'items'}` because
219
+ * `intersects({'items.length'}, {'items'}) = false`.
220
+ *
221
+ * Example: for leaf path `'a.b.c'`, adds `'a.b'` and `'a'` (but NOT `''`
222
+ * root, since a root change would be captured by ALL_PATHS from the source
223
+ * when there are ≤1 consumers, and adding `''` would make every field change
224
+ * wake this consumer regardless of what it actually reads).
225
+ */
226
+ function expandWithAncestors(paths, interner) {
227
+ if (paths === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
228
+ const leafPaths = paths;
229
+ if (leafPaths.size === 0) return paths;
230
+ const expanded = new Set(leafPaths);
231
+ for (const id of leafPaths) {
232
+ const str = interner.lookup(id);
233
+ let idx = str.lastIndexOf(".");
234
+ while (idx > 0) {
235
+ const ancestor = str.slice(0, idx);
236
+ expanded.add(interner.intern(ancestor));
237
+ idx = ancestor.lastIndexOf(".");
238
+ }
239
+ }
240
+ return expanded;
241
+ }
169
242
  let globalConfig = {};
170
243
  /**
171
244
  * Configure global defaults for `@blac/react` hooks.
@@ -182,6 +255,6 @@ function configureBlacReact(config) {
182
255
  exports.BlocProvider = BlocProvider;
183
256
  exports.configureBlacReact = configureBlacReact;
184
257
  exports.useBloc = useBloc;
185
- exports.useInstanceIdFromContext = useInstanceIdFromContext;
258
+ exports.useProvidedArgs = useProvidedArgs;
186
259
 
187
260
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["ALL_PATHS"],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n type ReactElement,\n type ReactNode,\n} from 'react';\n\nconst InstanceIdContext = createContext<string | undefined>(undefined);\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps {\n /**\n * Instance id that descendants' `useBloc(C)` calls will resolve to when no\n * explicit `instanceId` is given.\n */\n instanceId: string | number;\n children: ReactNode;\n}\n\n/**\n * Provides an `instanceId` to descendant `useBloc` calls via React context.\n *\n * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id\n * supplied here. An explicit `instanceId` on the `useBloc` call still wins.\n * `autoInstance` / `static isolated = true` also override the context, because\n * those force a per-mount auto-key.\n *\n * @example\n * ```tsx\n * <BlocProvider instanceId=\"customer-42\">\n * <CustomerView />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider({\n instanceId,\n children,\n}: BlocProviderProps): ReactElement {\n return (\n <InstanceIdContext.Provider value={String(instanceId)}>\n {children}\n </InstanceIdContext.Provider>\n );\n}\n\n/**\n * Reads the nearest {@link BlocProvider} instance id, or `undefined` when\n * called outside a provider.\n */\nexport function useInstanceIdFromContext(): string | undefined {\n return useContext(InstanceIdContext);\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n acquire,\n release,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useInstanceIdFromContext } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is the explicit `options.instanceId`, then\n * the surrounding {@link BlocProvider} context, then the default key.\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Named instance\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const args = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const argsRef = useRef(args);\n argsRef.current = args;\n const argsKey = args === undefined ? undefined : JSON.stringify(args);\n\n const explicitInstanceId = options?.instanceId;\n const ctxInstanceId = useInstanceIdFromContext();\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n const explicitKey =\n explicitInstanceId !== undefined\n ? String(explicitInstanceId)\n : ctxInstanceId;\n const resolvedKey = resolveInstanceKey(\n BlocClass,\n explicitKey,\n argsRef.current,\n );\n const refId = `useBloc@${consumerId}`;\n const instance = acquire(\n BlocClass,\n resolvedKey,\n refId,\n argsRef.current,\n ) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, explicitInstanceId, ctxInstanceId, argsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: wake on any change that intersects the recorded paths.\n // `() => pathRef.current` is called by the channel on every flush, so we\n // always see the latest interest without re-subscribing.\n const unsub = channel.subscribe(\n () => pathRef.current,\n () => force(),\n );\n // Also register the consumer paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits the proxy has been touched and pathRef.current\n // is the consumer's actual interest. Re-register with the container so the\n // skeleton reflects the latest paths. useLayoutEffect runs before the\n // browser paints (and before any emit triggered by another effect), so the\n // skeleton is fresh by the time the next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["ALL_PATHS"],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n PathInterner,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n const ownArgsKey =\n ownArgs === undefined ? undefined : JSON.stringify(ownArgs);\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n const providerArgsKey =\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs);\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const refId = `useBloc@${consumerId}`;\n const registry = getRegistry();\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: true,\n refId,\n args: effectiveArgs,\n }) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS their ancestor paths, so that when\n // `patch` marks a parent (e.g. 'items') and the consumer tracked a child\n // (e.g. 'items.length'), the intersection still fires. Updated in\n // useLayoutEffect after each render once pathRef.current is populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n getRegistry().release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n container.registerConsumerPaths(consumerId, paths);\n // Expand interest to include ancestor paths so that `patch`-marked parent\n // paths (e.g. 'items') wake a consumer that tracked a child (e.g.\n // 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include all ancestor paths of every tracked path.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` marks parent paths (e.g. `'items'`) since it\n * only walks the patch shape — not individual array elements or nested\n * descendants. Without ancestor expansion, a channel subscriber with interest\n * `{'items.length'}` would miss a `patch`-triggered `dirty={'items'}` because\n * `intersects({'items.length'}, {'items'}) = false`.\n *\n * Example: for leaf path `'a.b.c'`, adds `'a.b'` and `'a'` (but NOT `''`\n * root, since a root change would be captured by ALL_PATHS from the source\n * when there are ≤1 consumers, and adding `''` would make every field change\n * wake this consumer regardless of what it actually reads).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments: 'a.b.c' → add 'a.b', 'a'\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.intern(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Clean integration between React and StateContainer architecture.
5
5
  * Constructor-based API with automatic type inference.
6
- * Supports concurrent (useSyncExternalStore) mode for optimal performance.
6
+ * Re-renders are driven by per-consumer, path-scoped channel subscriptions.
7
7
  */
8
8
  export { useBloc } from './useBloc';
9
9
  export { configureBlacReact } from './config';
10
10
  export type { BlacReactConfig } from './config';
11
11
  export type { UseBlocOptions, UseBlocReturn } from './types';
12
- export { BlocProvider, useInstanceIdFromContext, type BlocProviderProps, } from './BlocProvider';
12
+ export { BlocProvider, useProvidedArgs, type BlocProviderProps, } from './BlocProvider';
package/dist/index.d.ts CHANGED
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Clean integration between React and StateContainer architecture.
5
5
  * Constructor-based API with automatic type inference.
6
- * Supports concurrent (useSyncExternalStore) mode for optimal performance.
6
+ * Re-renders are driven by per-consumer, path-scoped channel subscriptions.
7
7
  */
8
8
  export { useBloc } from './useBloc';
9
9
  export { configureBlacReact } from './config';
10
10
  export type { BlacReactConfig } from './config';
11
11
  export type { UseBlocOptions, UseBlocReturn } from './types';
12
- export { BlocProvider, useInstanceIdFromContext, type BlocProviderProps, } from './BlocProvider';
12
+ export { BlocProvider, useProvidedArgs, type BlocProviderProps, } from './BlocProvider';
package/dist/index.js CHANGED
@@ -1,36 +1,59 @@
1
1
  import { createContext, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef } from "react";
2
- import { acquire, release, resolveInstanceKey } from "@blac/core";
2
+ import { getRegistry, resolveInstanceKey } from "@blac/core";
3
3
  import { ALL_PATHS, emptyPathSet, trackRender } from "@dirtytalk/structural";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/BlocProvider.tsx
6
- const InstanceIdContext = createContext(void 0);
6
+ const ProvidedArgsContext = createContext(/* @__PURE__ */ new Map());
7
7
  /**
8
- * Provides an `instanceId` to descendant `useBloc` calls via React context.
8
+ * Provides args to descendant `useBloc` calls for a specific bloc class via
9
+ * React context.
9
10
  *
10
- * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id
11
- * supplied here. An explicit `instanceId` on the `useBloc` call still wins.
12
- * `autoInstance` / `static isolated = true` also override the context, because
13
- * those force a per-mount auto-key.
11
+ * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the
12
+ * args supplied here. Own `args` on the `useBloc` call always win.
13
+ *
14
+ * 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.
14
17
  *
15
18
  * @example
16
19
  * ```tsx
17
- * <BlocProvider instanceId="customer-42">
18
- * <CustomerView />
20
+ * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>
21
+ * <UserProfile />
22
+ * </BlocProvider>
23
+ * ```
24
+ *
25
+ * @example Per-mount private instance
26
+ * ```tsx
27
+ * const id = useId();
28
+ * <BlocProvider bloc={CartBloc} args={{ _id: id }}>
29
+ * <CartWidget />
19
30
  * </BlocProvider>
20
31
  * ```
21
32
  */
22
- function BlocProvider({ instanceId, children }) {
23
- return /* @__PURE__ */ jsx(InstanceIdContext.Provider, {
24
- value: String(instanceId),
33
+ function BlocProvider({ bloc, args, children }) {
34
+ const parentMap = useContext(ProvidedArgsContext);
35
+ const mergedMap = useMemo(() => {
36
+ const next = new Map(parentMap);
37
+ next.set(bloc, args);
38
+ return next;
39
+ }, [
40
+ parentMap,
41
+ bloc,
42
+ args
43
+ ]);
44
+ return /* @__PURE__ */ jsx(ProvidedArgsContext.Provider, {
45
+ value: mergedMap,
25
46
  children
26
47
  });
27
48
  }
28
49
  /**
29
- * Reads the nearest {@link BlocProvider} instance id, or `undefined` when
30
- * called outside a provider.
50
+ * Returns the args provided by the nearest {@link BlocProvider} for the given
51
+ * bloc class, or `undefined` when called outside a matching provider.
52
+ *
53
+ * Used by `useBloc` to inherit provider args when no own `args` are given.
31
54
  */
32
- function useInstanceIdFromContext() {
33
- return useContext(InstanceIdContext);
55
+ function useProvidedArgs(BlocClass) {
56
+ return useContext(ProvidedArgsContext).get(BlocClass);
34
57
  }
35
58
  //#endregion
36
59
  //#region src/useBloc.ts
@@ -50,12 +73,19 @@ let nextConsumerId = 0;
50
73
  *
51
74
  * Lifecycle:
52
75
  * - The bloc is acquired from the registry on mount and released on
53
- * unmount. The instance key is the explicit `options.instanceId`, then
54
- * the surrounding {@link BlocProvider} context, then the default key.
76
+ * unmount. The instance key is derived from `options.args` (own args),
77
+ * then the surrounding {@link BlocProvider} context args for this bloc,
78
+ * then the default key (no args).
55
79
  * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`
56
80
  * fires *before* the registry releases its ref, so the bloc is still
57
81
  * alive when the callback runs.
58
82
  *
83
+ * Per-mount private instance:
84
+ * ```ts
85
+ * const id = useId();
86
+ * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });
87
+ * ```
88
+ *
59
89
  * @template T - The state container constructor type (inferred from BlocClass)
60
90
  * @param BlocClass - The state container class to connect to
61
91
  * @param options - Configuration options
@@ -73,9 +103,9 @@ let nextConsumerId = 0;
73
103
  * });
74
104
  * ```
75
105
  *
76
- * @example Named instance
106
+ * @example Args-based shared instance
77
107
  * ```ts
78
- * const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });
108
+ * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });
79
109
  * ```
80
110
  */
81
111
  function useBloc(BlocClass, options) {
@@ -90,26 +120,34 @@ function useBloc(BlocClass, options) {
90
120
  onMountRef.current = options?.onMount;
91
121
  const onUnmountRef = useRef(options?.onUnmount);
92
122
  onUnmountRef.current = options?.onUnmount;
93
- const args = options?.args;
94
- const argsRef = useRef(args);
95
- argsRef.current = args;
96
- const argsKey = args === void 0 ? void 0 : JSON.stringify(args);
97
- const explicitInstanceId = options?.instanceId;
98
- const ctxInstanceId = useInstanceIdFromContext();
123
+ const ownArgs = options?.args;
124
+ const ownArgsRef = useRef(ownArgs);
125
+ ownArgsRef.current = ownArgs;
126
+ const ownArgsKey = ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs);
127
+ const providerArgs = useProvidedArgs(BlocClass);
128
+ const providerArgsRef = useRef(providerArgs);
129
+ providerArgsRef.current = providerArgs;
99
130
  const { bloc, instanceKey } = useMemo(() => {
100
- const resolvedKey = resolveInstanceKey(BlocClass, explicitInstanceId !== void 0 ? String(explicitInstanceId) : ctxInstanceId, argsRef.current);
131
+ const effectiveArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
132
+ const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);
133
+ const refId = `useBloc@${consumerId}`;
101
134
  return {
102
- bloc: acquire(BlocClass, resolvedKey, `useBloc@${consumerId}`, argsRef.current),
135
+ bloc: getRegistry().acquire(BlocClass, resolvedKey, {
136
+ canCreate: true,
137
+ countRef: true,
138
+ refId,
139
+ args: effectiveArgs
140
+ }),
103
141
  instanceKey: resolvedKey
104
142
  };
105
143
  }, [
106
144
  BlocClass,
107
- explicitInstanceId,
108
- ctxInstanceId,
109
- argsKey
145
+ ownArgsKey,
146
+ providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
110
147
  ]);
111
148
  const [, force] = useReducer((x) => x + 1, 0);
112
149
  const pathRef = useRef(emptyPathSet());
150
+ const expandedInterestRef = useRef(emptyPathSet());
113
151
  const lastSelectionRef = useRef(null);
114
152
  useEffect(() => {
115
153
  const channel = bloc.channel;
@@ -125,7 +163,7 @@ function useBloc(BlocClass, options) {
125
163
  lastSelectionRef.current = next;
126
164
  force();
127
165
  });
128
- const unsub = channel.subscribe(() => pathRef.current, () => force());
166
+ const unsub = channel.subscribe(() => expandedInterestRef.current, () => force());
129
167
  bloc.registerConsumerPaths(consumerId, pathRef.current);
130
168
  return () => {
131
169
  unsub();
@@ -136,7 +174,8 @@ function useBloc(BlocClass, options) {
136
174
  onMountRef.current?.(bloc);
137
175
  return () => {
138
176
  onUnmountRef.current?.(bloc);
139
- release(BlocClass, instanceKey, false, `useBloc@${consumerId}`);
177
+ const refId = `useBloc@${consumerId}`;
178
+ getRegistry().release(BlocClass, instanceKey, false, refId);
140
179
  };
141
180
  }, [bloc, instanceKey]);
142
181
  const rawState = bloc.state;
@@ -151,7 +190,10 @@ function useBloc(BlocClass, options) {
151
190
  }
152
191
  useLayoutEffect(() => {
153
192
  if (selectRef.current !== void 0) return;
154
- bloc.registerConsumerPaths(consumerId, pathRef.current);
193
+ const container = bloc;
194
+ const paths = pathRef.current;
195
+ container.registerConsumerPaths(consumerId, paths);
196
+ expandedInterestRef.current = expandWithAncestors(paths, container.interner);
155
197
  });
156
198
  return [
157
199
  state,
@@ -165,6 +207,37 @@ const shallowArrayEqual = (a, b) => {
165
207
  for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
166
208
  return true;
167
209
  };
210
+ /**
211
+ * Expand a PathSet to include all ancestor paths of every tracked path.
212
+ *
213
+ * The auto-tracker records leaf paths (e.g. `'items.length'`), but
214
+ * `StructuralContainer.patch` marks parent paths (e.g. `'items'`) since it
215
+ * only walks the patch shape — not individual array elements or nested
216
+ * descendants. Without ancestor expansion, a channel subscriber with interest
217
+ * `{'items.length'}` would miss a `patch`-triggered `dirty={'items'}` because
218
+ * `intersects({'items.length'}, {'items'}) = false`.
219
+ *
220
+ * Example: for leaf path `'a.b.c'`, adds `'a.b'` and `'a'` (but NOT `''`
221
+ * root, since a root change would be captured by ALL_PATHS from the source
222
+ * when there are ≤1 consumers, and adding `''` would make every field change
223
+ * wake this consumer regardless of what it actually reads).
224
+ */
225
+ function expandWithAncestors(paths, interner) {
226
+ if (paths === ALL_PATHS) return ALL_PATHS;
227
+ const leafPaths = paths;
228
+ if (leafPaths.size === 0) return paths;
229
+ const expanded = new Set(leafPaths);
230
+ for (const id of leafPaths) {
231
+ const str = interner.lookup(id);
232
+ let idx = str.lastIndexOf(".");
233
+ while (idx > 0) {
234
+ const ancestor = str.slice(0, idx);
235
+ expanded.add(interner.intern(ancestor));
236
+ idx = ancestor.lastIndexOf(".");
237
+ }
238
+ }
239
+ return expanded;
240
+ }
168
241
  let globalConfig = {};
169
242
  /**
170
243
  * Configure global defaults for `@blac/react` hooks.
@@ -178,6 +251,6 @@ function configureBlacReact(config) {
178
251
  };
179
252
  }
180
253
  //#endregion
181
- export { BlocProvider, configureBlacReact, useBloc, useInstanceIdFromContext };
254
+ export { BlocProvider, configureBlacReact, useBloc, useProvidedArgs };
182
255
 
183
256
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n type ReactElement,\n type ReactNode,\n} from 'react';\n\nconst InstanceIdContext = createContext<string | undefined>(undefined);\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps {\n /**\n * Instance id that descendants' `useBloc(C)` calls will resolve to when no\n * explicit `instanceId` is given.\n */\n instanceId: string | number;\n children: ReactNode;\n}\n\n/**\n * Provides an `instanceId` to descendant `useBloc` calls via React context.\n *\n * Descendants calling `useBloc(C)` without an `instanceId` resolve to the id\n * supplied here. An explicit `instanceId` on the `useBloc` call still wins.\n * `autoInstance` / `static isolated = true` also override the context, because\n * those force a per-mount auto-key.\n *\n * @example\n * ```tsx\n * <BlocProvider instanceId=\"customer-42\">\n * <CustomerView />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider({\n instanceId,\n children,\n}: BlocProviderProps): ReactElement {\n return (\n <InstanceIdContext.Provider value={String(instanceId)}>\n {children}\n </InstanceIdContext.Provider>\n );\n}\n\n/**\n * Reads the nearest {@link BlocProvider} instance id, or `undefined` when\n * called outside a provider.\n */\nexport function useInstanceIdFromContext(): string | undefined {\n return useContext(InstanceIdContext);\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n acquire,\n release,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useInstanceIdFromContext } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is the explicit `options.instanceId`, then\n * the surrounding {@link BlocProvider} context, then the default key.\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Named instance\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const args = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const argsRef = useRef(args);\n argsRef.current = args;\n const argsKey = args === undefined ? undefined : JSON.stringify(args);\n\n const explicitInstanceId = options?.instanceId;\n const ctxInstanceId = useInstanceIdFromContext();\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n const explicitKey =\n explicitInstanceId !== undefined\n ? String(explicitInstanceId)\n : ctxInstanceId;\n const resolvedKey = resolveInstanceKey(\n BlocClass,\n explicitKey,\n argsRef.current,\n );\n const refId = `useBloc@${consumerId}`;\n const instance = acquire(\n BlocClass,\n resolvedKey,\n refId,\n argsRef.current,\n ) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, explicitInstanceId, ctxInstanceId, argsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: wake on any change that intersects the recorded paths.\n // `() => pathRef.current` is called by the channel on every flush, so we\n // always see the latest interest without re-subscribing.\n const unsub = channel.subscribe(\n () => pathRef.current,\n () => force(),\n );\n // Also register the consumer paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits the proxy has been touched and pathRef.current\n // is the consumer's actual interest. Re-register with the container so the\n // skeleton reflects the latest paths. useLayoutEffect runs before the\n // browser paints (and before any emit triggered by another effect), so the\n // skeleton is fresh by the time the next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n PathInterner,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n const ownArgsKey =\n ownArgs === undefined ? undefined : JSON.stringify(ownArgs);\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n const providerArgsKey =\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs);\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const refId = `useBloc@${consumerId}`;\n const registry = getRegistry();\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: true,\n refId,\n args: effectiveArgs,\n }) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS their ancestor paths, so that when\n // `patch` marks a parent (e.g. 'items') and the consumer tracked a child\n // (e.g. 'items.length'), the intersection still fires. Updated in\n // useLayoutEffect after each render once pathRef.current is populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n getRegistry().release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n container.registerConsumerPaths(consumerId, paths);\n // Expand interest to include ancestor paths so that `patch`-marked parent\n // paths (e.g. 'items') wake a consumer that tracked a child (e.g.\n // 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include all ancestor paths of every tracked path.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` marks parent paths (e.g. `'items'`) since it\n * only walks the patch shape — not individual array elements or nested\n * descendants. Without ancestor expansion, a channel subscriber with interest\n * `{'items.length'}` would miss a `patch`-triggered `dirty={'items'}` because\n * `intersects({'items.length'}, {'items'}) = false`.\n *\n * Example: for leaf path `'a.b.c'`, adds `'a.b'` and `'a'` (but NOT `''`\n * root, since a root change would be captured by ALL_PATHS from the source\n * when there are ≤1 consumers, and adding `''` would make every field change\n * wake this consumer regardless of what it actually reads).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments: 'a.b.c' → add 'a.b', 'a'\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.intern(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
package/dist/testing.cjs CHANGED
@@ -4,11 +4,14 @@ let _blac_core_testing = require("@blac/core/testing");
4
4
  let _testing_library_react = require("@testing-library/react");
5
5
  //#region src/testing.ts
6
6
  function renderWithBloc(ui, options) {
7
- const { bloc: BlocClass, instanceKey, ...stubOptions } = options;
7
+ const { bloc: BlocClass, args, ...stubOptions } = options;
8
8
  const previous = (0, _blac_core.getRegistry)();
9
9
  (0, _blac_core.setRegistry)(new _blac_core.StateContainerRegistry());
10
- const instance = (0, _blac_core_testing.createCubitStub)(BlocClass, stubOptions);
11
- (0, _blac_core_testing.registerOverride)(BlocClass, instance, instanceKey);
10
+ const instance = (0, _blac_core_testing.createCubitStub)(BlocClass, {
11
+ ...stubOptions,
12
+ args
13
+ });
14
+ (0, _blac_core_testing.registerOverride)(BlocClass, instance, args);
12
15
  let renderResult;
13
16
  try {
14
17
  renderResult = (0, _testing_library_react.render)(ui);
@@ -1 +1 @@
1
- {"version":3,"file":"testing.cjs","names":["StateContainerRegistry"],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n instanceKey?: string;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, instanceKey, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n const instance = createCubitStub(BlocClass, stubOptions);\n registerOverride(BlocClass, instance, instanceKey);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;;AAkBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,aAAa,GAAG,gBAAgB;CAEzD,MAAM,YAAA,GAAA,WAAA,cAAwB;CAE9B,CAAA,GAAA,WAAA,aAAY,IADaA,WAAAA,wBACD,CAAC;CAEzB,MAAM,YAAA,GAAA,mBAAA,iBAA2B,WAAW,YAAY;CACxD,CAAA,GAAA,mBAAA,kBAAiB,WAAW,UAAU,YAAY;CAElD,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,GAAG;UAClB,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,CAAA,GAAA,WAAA,aAAY,SAAS;;CAGvB,OAAO;EACL,GAAG;EACH,MAAM;EACP;;AAGH,SAAgB,mBACd,IACA,OACc;CACd,MAAM,YAAA,GAAA,WAAA,cAAwB;CAC9B,MAAM,eAAe,IAAIA,WAAAA,wBAAwB;CACjD,CAAA,GAAA,WAAA,aAAY,aAAa;CAEzB,IAAI;EACF,MAAM,aAAa;UACZ,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,SAAS;EACrB,MAAM;;CAGR,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,GAAG;UAClB,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,CAAA,GAAA,WAAA,aAAY,SAAS;;CAGvB,OAAO"}
1
+ {"version":3,"file":"testing.cjs","names":["StateContainerRegistry"],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n /** Args used to resolve which instance the stub is registered under. */\n args?: ExtractArgs<T>;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, args, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n // Pass args explicitly so createCubitStub calls initConfig → init().\n const instance = createCubitStub(BlocClass, { ...stubOptions, args } as any);\n registerOverride(BlocClass, instance, args);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;;AAmBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,MAAM,GAAG,gBAAgB;CAElD,MAAM,YAAA,GAAA,WAAA,aAAuB;CAE7B,CAAA,GAAA,WAAA,aAAY,IADaA,WAAAA,uBACF,CAAC;CAGxB,MAAM,YAAA,GAAA,mBAAA,iBAA2B,WAAW;EAAE,GAAG;EAAa;CAAK,CAAQ;CAC3E,CAAA,GAAA,mBAAA,kBAAiB,WAAW,UAAU,IAAI;CAE1C,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,EAAE;CAC1B,SAAS,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,CAAA,GAAA,WAAA,aAAY,QAAQ;CACtB;CAEA,OAAO;EACL,GAAG;EACH,MAAM;CACR;AACF;AAEA,SAAgB,mBACd,IACA,OACc;CACd,MAAM,YAAA,GAAA,WAAA,aAAuB;CAC7B,MAAM,eAAe,IAAIA,WAAAA,uBAAuB;CAChD,CAAA,GAAA,WAAA,aAAY,YAAY;CAExB,IAAI;EACF,MAAM,YAAY;CACpB,SAAS,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,QAAQ;EACpB,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,EAAE;CAC1B,SAAS,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,CAAA,GAAA,WAAA,aAAY,QAAQ;CACtB;CAEA,OAAO;AACT"}
@@ -1,11 +1,12 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { RenderResult } from '@testing-library/react';
3
- import type { StateContainerConstructor } from '@blac/core';
3
+ import type { ExtractArgs, StateContainerConstructor } from '@blac/core';
4
4
  import { StateContainerRegistry } from '@blac/core';
5
5
  import { type CubitStubOptions } from '@blac/core/testing';
6
6
  interface RenderWithBlocOptions<T extends StateContainerConstructor> extends CubitStubOptions<T> {
7
7
  bloc: T;
8
- instanceKey?: string;
8
+ /** Args used to resolve which instance the stub is registered under. */
9
+ args?: ExtractArgs<T>;
9
10
  }
10
11
  export declare function renderWithBloc<T extends StateContainerConstructor>(ui: ReactElement, options: RenderWithBlocOptions<T>): RenderResult & {
11
12
  bloc: InstanceType<T>;
package/dist/testing.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { RenderResult } from '@testing-library/react';
3
- import type { StateContainerConstructor } from '@blac/core';
3
+ import type { ExtractArgs, StateContainerConstructor } from '@blac/core';
4
4
  import { StateContainerRegistry } from '@blac/core';
5
5
  import { type CubitStubOptions } from '@blac/core/testing';
6
6
  interface RenderWithBlocOptions<T extends StateContainerConstructor> extends CubitStubOptions<T> {
7
7
  bloc: T;
8
- instanceKey?: string;
8
+ /** Args used to resolve which instance the stub is registered under. */
9
+ args?: ExtractArgs<T>;
9
10
  }
10
11
  export declare function renderWithBloc<T extends StateContainerConstructor>(ui: ReactElement, options: RenderWithBlocOptions<T>): RenderResult & {
11
12
  bloc: InstanceType<T>;
package/dist/testing.js CHANGED
@@ -3,11 +3,14 @@ import { createCubitStub, registerOverride } from "@blac/core/testing";
3
3
  import { render } from "@testing-library/react";
4
4
  //#region src/testing.ts
5
5
  function renderWithBloc(ui, options) {
6
- const { bloc: BlocClass, instanceKey, ...stubOptions } = options;
6
+ const { bloc: BlocClass, args, ...stubOptions } = options;
7
7
  const previous = getRegistry();
8
8
  setRegistry(new StateContainerRegistry());
9
- const instance = createCubitStub(BlocClass, stubOptions);
10
- registerOverride(BlocClass, instance, instanceKey);
9
+ const instance = createCubitStub(BlocClass, {
10
+ ...stubOptions,
11
+ args
12
+ });
13
+ registerOverride(BlocClass, instance, args);
11
14
  let renderResult;
12
15
  try {
13
16
  renderResult = render(ui);
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n instanceKey?: string;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, instanceKey, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n const instance = createCubitStub(BlocClass, stubOptions);\n registerOverride(BlocClass, instance, instanceKey);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;AAkBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,aAAa,GAAG,gBAAgB;CAEzD,MAAM,WAAW,aAAa;CAE9B,YAAY,IADa,wBACD,CAAC;CAEzB,MAAM,WAAW,gBAAgB,WAAW,YAAY;CACxD,iBAAiB,WAAW,UAAU,YAAY;CAElD,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,GAAG;UAClB,GAAG;EACV,YAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,YAAY,SAAS;;CAGvB,OAAO;EACL,GAAG;EACH,MAAM;EACP;;AAGH,SAAgB,mBACd,IACA,OACc;CACd,MAAM,WAAW,aAAa;CAC9B,MAAM,eAAe,IAAI,wBAAwB;CACjD,YAAY,aAAa;CAEzB,IAAI;EACF,MAAM,aAAa;UACZ,GAAG;EACV,YAAY,SAAS;EACrB,MAAM;;CAGR,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,GAAG;UAClB,GAAG;EACV,YAAY,SAAS;EACrB,MAAM;;CAGR,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,iBAAiB;EACjB,YAAY,SAAS;;CAGvB,OAAO"}
1
+ {"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n /** Args used to resolve which instance the stub is registered under. */\n args?: ExtractArgs<T>;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, args, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n // Pass args explicitly so createCubitStub calls initConfig → init().\n const instance = createCubitStub(BlocClass, { ...stubOptions, args } as any);\n registerOverride(BlocClass, instance, args);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;AAmBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,MAAM,GAAG,gBAAgB;CAElD,MAAM,WAAW,YAAY;CAE7B,YAAY,IADa,uBACF,CAAC;CAGxB,MAAM,WAAW,gBAAgB,WAAW;EAAE,GAAG;EAAa;CAAK,CAAQ;CAC3E,iBAAiB,WAAW,UAAU,IAAI;CAE1C,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,EAAE;CAC1B,SAAS,GAAG;EACV,YAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,YAAY,QAAQ;CACtB;CAEA,OAAO;EACL,GAAG;EACH,MAAM;CACR;AACF;AAEA,SAAgB,mBACd,IACA,OACc;CACd,MAAM,WAAW,YAAY;CAC7B,MAAM,eAAe,IAAI,uBAAuB;CAChD,YAAY,YAAY;CAExB,IAAI;EACF,MAAM,YAAY;CACpB,SAAS,GAAG;EACV,YAAY,QAAQ;EACpB,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,EAAE;CAC1B,SAAS,GAAG;EACV,YAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,YAAY,QAAQ;CACtB;CAEA,OAAO;AACT"}
package/dist/types.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import type { ExtractArgs, ExtractState, InstanceReadonlyState, StateContainerConstructor } from '@blac/core';
2
2
  import type { RefObject } from 'react';
3
3
  /**
4
- * Conditional `args` field: required when the bloc declares Args, forbidden
5
- * (type `never`) when the bloc uses the default `void` Args.
4
+ * Conditional `args` field:
5
+ * - When the bloc uses the default `void` Args: args is forbidden (type `never`).
6
+ * - When the bloc declares Args: args is optional (may be inherited from a
7
+ * `BlocProvider` ancestor at runtime; omitting it resolves to the provider
8
+ * args or the default key).
6
9
  */
7
10
  type ArgsOption<T extends StateContainerConstructor> = ExtractArgs<T> extends void ? {
8
11
  args?: never;
9
12
  } : {
10
- args: ExtractArgs<T>;
13
+ args?: ExtractArgs<T>;
11
14
  };
12
15
  /**
13
16
  * Configuration options for {@link useBloc}.
@@ -15,16 +18,15 @@ type ArgsOption<T extends StateContainerConstructor> = ExtractArgs<T> extends vo
15
18
  * @template TBloc - The state container constructor type
16
19
  *
17
20
  * @remarks
21
+ * Instance identity is derived entirely from `args`. For a per-mount private
22
+ * instance (like the old `instanceId: useId()` pattern) pass a stable unique
23
+ * object: `{ args: { _id: useId() } }`.
24
+ *
18
25
  * The `dependencies` option from BlaC v1 has been removed — use `select`
19
26
  * instead. Codemod: rename `dependencies` → `select`; the return value is
20
27
  * still a tuple/array compared per-index via `Object.is`.
21
28
  */
22
29
  export type UseBlocOptions<TBloc extends StateContainerConstructor> = ArgsOption<TBloc> & {
23
- /**
24
- * Per-consumer instance identifier. Overrides any `instanceId` provided
25
- * by an ancestor {@link BlocProvider}.
26
- */
27
- instanceId?: string | number;
28
30
  /**
29
31
  * Per-consumer re-render selector. When provided, the hook re-renders
30
32
  * only when the returned array's elements change (Object.is per index).
package/dist/useBloc.d.ts CHANGED
@@ -15,12 +15,19 @@ import type { UseBlocOptions, UseBlocReturn } from './types';
15
15
  *
16
16
  * Lifecycle:
17
17
  * - The bloc is acquired from the registry on mount and released on
18
- * unmount. The instance key is the explicit `options.instanceId`, then
19
- * the surrounding {@link BlocProvider} context, then the default key.
18
+ * unmount. The instance key is derived from `options.args` (own args),
19
+ * then the surrounding {@link BlocProvider} context args for this bloc,
20
+ * then the default key (no args).
20
21
  * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`
21
22
  * fires *before* the registry releases its ref, so the bloc is still
22
23
  * alive when the callback runs.
23
24
  *
25
+ * Per-mount private instance:
26
+ * ```ts
27
+ * const id = useId();
28
+ * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });
29
+ * ```
30
+ *
24
31
  * @template T - The state container constructor type (inferred from BlocClass)
25
32
  * @param BlocClass - The state container class to connect to
26
33
  * @param options - Configuration options
@@ -38,9 +45,9 @@ import type { UseBlocOptions, UseBlocReturn } from './types';
38
45
  * });
39
46
  * ```
40
47
  *
41
- * @example Named instance
48
+ * @example Args-based shared instance
42
49
  * ```ts
43
- * const [state, bloc] = useBloc(MyBloc, { instanceId: 'cart-42' });
50
+ * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });
44
51
  * ```
45
52
  */
46
53
  export declare function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState<T>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blac/react",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "license": "MIT",
5
5
  "author": "Brendan Mullins <jsnanigans@gmail.com>",
6
6
  "description": "React bindings for BlaC — useBloc hook with automatic re-render optimization",
@@ -64,14 +64,27 @@
64
64
  "bloc",
65
65
  "bloc-pattern"
66
66
  ],
67
+ "size-limit": [
68
+ {
69
+ "name": "@blac/react (ESM, brotli, excl. peers)",
70
+ "path": "dist/index.js",
71
+ "limit": "3.5 kB",
72
+ "ignore": [
73
+ "react",
74
+ "react-dom",
75
+ "@blac/core",
76
+ "@types/react"
77
+ ]
78
+ }
79
+ ],
67
80
  "dependencies": {
68
- "@dirtytalk/structural": "^0.0.2"
81
+ "@dirtytalk/structural": "^0.0.4"
69
82
  },
70
83
  "peerDependencies": {
71
84
  "@testing-library/react": "^14.0.0 || ^15.0.0 || ^16.0.0",
72
85
  "@types/react": "^18.0.0 || ^19.0.0",
73
86
  "react": "^18.0.0 || ^19.0.0",
74
- "@blac/core": "^2.0.14"
87
+ "@blac/core": "^2.0.16"
75
88
  },
76
89
  "peerDependenciesMeta": {
77
90
  "@types/react": {
@@ -85,24 +98,26 @@
85
98
  }
86
99
  },
87
100
  "devDependencies": {
101
+ "@size-limit/preset-small-lib": "^11.1.6",
102
+ "size-limit": "^11.1.6",
88
103
  "@testing-library/dom": "^10.4.1",
89
- "@testing-library/jest-dom": "^6.6.4",
90
- "@testing-library/react": "^16.3.0",
104
+ "@testing-library/jest-dom": "^6.9.1",
105
+ "@testing-library/react": "^16.3.2",
91
106
  "@testing-library/user-event": "^14.6.1",
92
- "@types/react": "^19.1.9",
93
- "@types/react-dom": "^19.1.7",
94
- "@vitejs/plugin-react": "^4.7.0",
107
+ "@types/react": "^19.2.15",
108
+ "@types/react-dom": "^19.2.3",
109
+ "@vitejs/plugin-react": "^6.0.2",
95
110
  "babel-plugin-react-compiler": "^1.0.0",
96
- "happy-dom": "^18.0.1",
97
- "jsdom": "^26.1.0",
98
- "publint": "^0.3.12",
99
- "react": "^19.1.1",
100
- "react-dom": "^19.1.1",
101
- "typescript": "^5.9.2",
111
+ "happy-dom": "^20.9.0",
112
+ "jsdom": "^29.1.1",
113
+ "publint": "^0.3.21",
114
+ "react": "^19.2.6",
115
+ "react-dom": "^19.2.6",
116
+ "typescript": "^6.0.3",
102
117
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",
118
+ "vite-plus": "^0.1.23",
103
119
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest",
104
- "vite-plus": "latest",
105
- "@blac/core": "2.0.14"
120
+ "@blac/core": "2.0.16"
106
121
  },
107
122
  "scripts": {
108
123
  "dev": "vp pack --watch",
@@ -121,6 +136,7 @@
121
136
  "test:both": "pnpm test && echo '\n\n=== Running tests WITH React Compiler ===\n' && pnpm test:compiler",
122
137
  "verify": "node ../../scripts/run-workspace-bin.mjs publint",
123
138
  "typecheck": "tsc --noEmit",
139
+ "size": "size-limit",
124
140
  "deploy": "pnpm publish --access public"
125
141
  }
126
142
  }