@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 +62 -49
- package/dist/BlocProvider.d.ts +33 -15
- package/dist/index.cjs +107 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +108 -35
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +6 -3
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -2
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +6 -3
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +10 -8
- package/dist/useBloc.d.ts +11 -4
- package/package.json +32 -16
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
|
|
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
|
|
58
|
-
|
|
59
|
-
| **`args`** | Typed creation data; derives instance identity | **Yes** (structural hash or `static key`) | Once at `init()`
|
|
60
|
-
| **`deps`** | Non-serializable refs, callbacks, handles
|
|
61
|
-
| **events** | Values that change over time or are late-bound | N/A
|
|
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** —
|
|
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
|
|
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
|
|
115
|
+
const ownerId = useId();
|
|
116
|
+
const [state, cubit] = useBloc(FileUploadCubit, { args: { endpoint } });
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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<
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
133
|
-
|
|
146
|
+
// Component A owns inputRef
|
|
147
|
+
cubitA[APPLY_DEPS](ownerIdA, { inputRef });
|
|
134
148
|
|
|
135
|
-
// Component B
|
|
136
|
-
|
|
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:**
|
|
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
|
|
199
|
-
|
|
|
200
|
-
| `args`
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
214
|
-
2.
|
|
215
|
-
3. **`static key(args)` → structural hash of `args`** — default when bloc declares `Args` (
|
|
216
|
-
4.
|
|
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<
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
266
|
+
// Configuration is currently empty; the tracking model is fixed and not configurable.
|
|
267
|
+
configureBlacReact({});
|
|
255
268
|
```
|
|
256
269
|
|
|
257
270
|
## Testing
|
package/dist/BlocProvider.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
* explicit `instanceId` is given.
|
|
8
|
+
* The bloc class whose args are being provided to descendants.
|
|
9
9
|
*/
|
|
10
|
-
|
|
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
|
|
19
|
+
* Provides args to descendant `useBloc` calls for a specific bloc class via
|
|
20
|
+
* React context.
|
|
15
21
|
*
|
|
16
|
-
* Descendants calling `useBloc(
|
|
17
|
-
* supplied here.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
24
|
-
* <
|
|
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({
|
|
44
|
+
export declare function BlocProvider<T extends StateContainerConstructor>({ bloc, args, children, }: BlocProviderProps<T>): ReactElement;
|
|
29
45
|
/**
|
|
30
|
-
*
|
|
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
|
|
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
|
|
7
|
+
const ProvidedArgsContext = (0, react.createContext)(/* @__PURE__ */ new Map());
|
|
8
8
|
/**
|
|
9
|
-
* Provides
|
|
9
|
+
* Provides args to descendant `useBloc` calls for a specific bloc class via
|
|
10
|
+
* React context.
|
|
10
11
|
*
|
|
11
|
-
* Descendants calling `useBloc(
|
|
12
|
-
* supplied here.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
19
|
-
* <
|
|
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({
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
*
|
|
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
|
|
34
|
-
return (0, react.useContext)(
|
|
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
|
|
55
|
-
* the surrounding {@link BlocProvider} context
|
|
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
|
|
107
|
+
* @example Args-based shared instance
|
|
78
108
|
* ```ts
|
|
79
|
-
* const [state, bloc] = useBloc(
|
|
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
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
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
|
|
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.
|
|
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
|
-
|
|
109
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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
|
|
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.
|
|
258
|
+
exports.useProvidedArgs = useProvidedArgs;
|
|
186
259
|
|
|
187
260
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":";;;;;;AAkBA,MAAM,uBAAA,GAAA,MAAA,+BAAqD,IAAI,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CpE,SAAgB,aAAkD,EAChE,MACA,MACA,YACqC;CACrC,MAAM,aAAA,GAAA,MAAA,YAAuB,mBAAmB;CAIhD,MAAM,aAAA,GAAA,MAAA,eAA0B;EAC9B,MAAM,OAAO,IAAI,IAAI,SAAS;EAC9B,KAAK,IAAI,MAAM,IAAI;EACnB,OAAO;CAET,GAAG;EAAC;EAAW;EAAM;CAAI,CAAC;CAE1B,OACE,iBAAA,GAAA,kBAAA,KAAC,oBAAoB,UAArB;EAA8B,OAAO;EAClC;CAC2B,CAAA;AAElC;;;;;;;AAQA,SAAgB,gBACd,WAC4B;CAE5B,QAAA,GAAA,MAAA,YADuB,mBACd,EAAE,IAAI,SAAS;AAC1B;;;ACpEA,IAAI,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDrB,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,gBAAA,GAAA,MAAA,QAAoC,CAAC,CAAC;CAK5C,MAAM,iBAAA,GAAA,MAAA,QAAsC,IAAI;CAChD,IAAI,cAAc,YAAY,MAC5B,cAAc,UAAU,WAAW;CAErC,MAAM,aAAa,cAAc;CAGjC,CAAA,GAAA,MAAA,OAAM;CAIN,MAAM,aAAA,GAAA,MAAA,QAAmB,SAAS,MAAM;CACxC,UAAU,UAAU,SAAS;CAC7B,MAAM,cAAA,GAAA,MAAA,QAAoB,SAAS,OAAO;CAC1C,WAAW,UAAU,SAAS;CAC9B,MAAM,gBAAA,GAAA,MAAA,QAAsB,SAAS,SAAS;CAC9C,aAAa,UAAU,SAAS;CAYhC,MAAM,UAAW,SAAmD;CACpE,MAAM,cAAA,GAAA,MAAA,QAAoB,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aACJ,YAAY,KAAA,IAAY,KAAA,IAAY,KAAK,UAAU,OAAO;CAG5D,MAAM,eAAe,gBAAgB,SAAS;CAC9C,MAAM,mBAAA,GAAA,MAAA,QAAyB,YAAY;CAC3C,gBAAgB,UAAU;CAI1B,MAAM,EAAE,MAAM,iBAAA,GAAA,MAAA,eAGL;EAEP,MAAM,gBACJ,WAAW,YAAY,KAAA,IACnB,WAAW,UACX,gBAAgB;EAEtB,MAAM,eAAA,GAAA,WAAA,oBAAiC,WAAW,aAAa;EAC/D,MAAM,QAAQ,WAAW;EAQzB,OAAO;GAAE,OAAA,GAAA,WAAA,aANe,EAAE,QAAQ,WAAW,aAAa;IACxD,WAAW;IACX,UAAU;IACV;IACA,MAAM;GACR,CACsB;GAAG,aAAa;EAAY;CAEpD,GAAG;EAAC;EAAW;EAvBb,iBAAiB,KAAA,IAAY,KAAA,IAAY,KAAK,UAAU,YAAY;CAuB5B,CAAC;CAU3C,MAAM,GAAG,UAAA,GAAA,MAAA,aAAqB,MAAc,IAAI,GAAG,CAAC;CACpD,MAAM,WAAA,GAAA,MAAA,SAAA,GAAA,sBAAA,cAAuC,CAAC;CAK9C,MAAM,uBAAA,GAAA,MAAA,SAAA,GAAA,sBAAA,cAAmD,CAAC;CAG1D,MAAM,oBAAA,GAAA,MAAA,QAA4C,IAAI;CAEtD,CAAA,GAAA,MAAA,iBAAgB;EAId,MAAM,UAAW,KAAmC;EAGpD,IAFqB,UAAU,YAAY,KAAA,GAqBzC,OAlBc,QAAQ,gBACdA,sBAAAA,iBACA;GACJ,MAAM,SAAS,UAAU;GACzB,IAAI,CAAC,QAAQ;IACX,MAAM;IACN;GACF;GACA,MAAM,OAAO,OACV,KAAmC,OACpC,IACF;GACA,MAAM,OAAO,iBAAiB;GAC9B,IAAI,SAAS,QAAQ,kBAAkB,MAAM,IAAI,GAAG;GACpD,iBAAiB,UAAU;GAC3B,MAAM;EACR,CAES;EAUb,MAAM,QAAQ,QAAQ,gBACd,oBAAoB,eACpB,MAAM,CACd;EAIA,KAAoC,sBAClC,YACA,QAAQ,OACV;EACA,aAAa;GACX,MAAM;GACN,KAAoC,mBAAmB,UAAU;EACnE;CACF,GAAG,CAAC,MAAM,UAAU,CAAC;CAQrB,CAAA,GAAA,MAAA,iBAAgB;EACd,WAAW,UAAU,IAAuB;EAC5C,aAAa;GACX,aAAa,UAAU,IAAuB;GAC9C,MAAM,QAAQ,WAAW;GACzB,CAAA,GAAA,WAAA,aAAY,EAAE,QAAQ,WAAW,aAAa,OAAO,KAAK;EAC5D;CAEF,GAAG,CAAC,MAAM,WAAW,CAAC;CAUtB,MAAM,WAAY,KAAmC;CACrD,IAAI;CACJ,IAAI,UAAU,YAAY,KAAA,GAAW;EACnC,QAAQ;EAGR,IAAI,iBAAiB,YAAY,MAC/B,iBAAiB,UAAU,UAAU,QACnC,UACA,IACF;CAEJ,OAAO;EACL,MAAM,WAAA,GAAA,sBAAA,aACJ,UACC,KAAmC,QACtC;EACA,QAAQ,QAAQ;EAChB,QAAQ,UAAU,QAAQ;CAQ5B;CAWA,CAAA,GAAA,MAAA,uBAAsB;EACpB,IAAI,UAAU,YAAY,KAAA,GAAW;EACrC,MAAM,YAAY;EAClB,MAAM,QAAQ,QAAQ;EACtB,UAAU,sBAAsB,YAAY,KAAK;EAIjD,oBAAoB,UAAU,oBAC5B,OACA,UAAU,QACZ;CACF,CAAC;CAED,OAAO;EACL;EACA;EACA;CACF;AACF;AAEA,MAAM,qBAAqB,GAAc,MAA0B;CACjE,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAC5B,IAAI,CAAC,OAAO,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO;CAErC,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAS,oBAAoB,OAAgB,UAAiC;CAC5E,IAAI,UAAUA,sBAAAA,WAAW,OAAOA,sBAAAA;CAChC,MAAM,YAAY;CAClB,IAAI,UAAU,SAAS,GAAG,OAAO;CAEjC,MAAM,WAAW,IAAI,IAAY,SAAS;CAC1C,KAAK,MAAM,MAAM,WAAW;EAC1B,MAAM,MAAM,SAAS,OAAO,EAAE;EAE9B,IAAI,MAAM,IAAI,YAAY,GAAG;EAC7B,OAAO,MAAM,GAAG;GACd,MAAM,WAAW,IAAI,MAAM,GAAG,GAAG;GACjC,SAAS,IAAI,SAAS,OAAO,QAAQ,CAAC;GACtC,MAAM,SAAS,YAAY,GAAG;EAChC;CACF;CACA,OAAO;AACT;ACjVA,IAAI,eAAgC,CAAmB;;;;;;AAOvD,SAAgB,mBAAmB,QAAwC;CACzE,eAAe;EAAE,GAAG;EAAc,GAAG;CAAO;AAC9C"}
|
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
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
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,
|
|
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 {
|
|
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
|
|
6
|
+
const ProvidedArgsContext = createContext(/* @__PURE__ */ new Map());
|
|
7
7
|
/**
|
|
8
|
-
* Provides
|
|
8
|
+
* Provides args to descendant `useBloc` calls for a specific bloc class via
|
|
9
|
+
* React context.
|
|
9
10
|
*
|
|
10
|
-
* Descendants calling `useBloc(
|
|
11
|
-
* supplied here.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
18
|
-
* <
|
|
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({
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
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
|
|
33
|
-
return useContext(
|
|
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
|
|
54
|
-
* the surrounding {@link BlocProvider} context
|
|
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
|
|
106
|
+
* @example Args-based shared instance
|
|
77
107
|
* ```ts
|
|
78
|
-
* const [state, bloc] = useBloc(
|
|
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
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
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
|
|
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,
|
|
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
|
-
|
|
108
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
11
|
-
|
|
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);
|
package/dist/testing.cjs.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/testing.d.cts
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
|
-
|
|
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
|
-
|
|
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,
|
|
6
|
+
const { bloc: BlocClass, args, ...stubOptions } = options;
|
|
7
7
|
const previous = getRegistry();
|
|
8
8
|
setRegistry(new StateContainerRegistry());
|
|
9
|
-
const instance = createCubitStub(BlocClass,
|
|
10
|
-
|
|
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);
|
package/dist/testing.js.map
CHANGED
|
@@ -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
|
|
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:
|
|
5
|
-
*
|
|
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
|
|
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
|
|
19
|
-
* the surrounding {@link BlocProvider} context
|
|
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
|
|
48
|
+
* @example Args-based shared instance
|
|
42
49
|
* ```ts
|
|
43
|
-
* const [state, bloc] = useBloc(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
90
|
-
"@testing-library/react": "^16.3.
|
|
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.
|
|
93
|
-
"@types/react-dom": "^19.
|
|
94
|
-
"@vitejs/plugin-react": "^
|
|
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": "^
|
|
97
|
-
"jsdom": "^
|
|
98
|
-
"publint": "^0.3.
|
|
99
|
-
"react": "^19.
|
|
100
|
-
"react-dom": "^19.
|
|
101
|
-
"typescript": "^
|
|
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
|
-
"
|
|
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
|
}
|