@blac/react 2.0.20 → 2.0.22
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 +41 -29
- package/dist/BlocProvider.d.cts +54 -0
- package/dist/BlocProvider.d.ts +3 -0
- package/dist/RegistryProvider.d.cts +31 -0
- package/dist/RegistryProvider.d.ts +31 -0
- package/dist/buildTrackedProxy.d.cts +22 -0
- package/dist/buildTrackedProxy.d.ts +12 -21
- package/dist/config.d.cts +31 -0
- package/dist/config.d.ts +3 -0
- package/dist/index.cjs +276 -202
- 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 +277 -204
- package/dist/index.js.map +1 -1
- package/dist/types.d.cts +59 -0
- package/dist/types.d.ts +5 -10
- package/dist/useBloc.d.cts +54 -0
- package/dist/useBloc.d.ts +3 -2
- package/dist/useBlocDeps.d.cts +19 -0
- package/dist/useBlocDeps.d.ts +19 -0
- package/package.json +24 -22
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ const ProvidedArgsContext = (0, react.createContext)(/* @__PURE__ */ new Map());
|
|
|
30
30
|
* <CartWidget />
|
|
31
31
|
* </BlocProvider>
|
|
32
32
|
* ```
|
|
33
|
+
* @public
|
|
33
34
|
*/
|
|
34
35
|
function BlocProvider({ bloc, args, children }) {
|
|
35
36
|
const parentMap = (0, react.useContext)(ProvidedArgsContext);
|
|
@@ -40,7 +41,7 @@ function BlocProvider({ bloc, args, children }) {
|
|
|
40
41
|
}, [
|
|
41
42
|
parentMap,
|
|
42
43
|
bloc,
|
|
43
|
-
args
|
|
44
|
+
(0, _blac_core.resolveInstanceKey)(bloc, args)
|
|
44
45
|
]);
|
|
45
46
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidedArgsContext.Provider, {
|
|
46
47
|
value: mergedMap,
|
|
@@ -52,69 +53,107 @@ function BlocProvider({ bloc, args, children }) {
|
|
|
52
53
|
* bloc class, or `undefined` when called outside a matching provider.
|
|
53
54
|
*
|
|
54
55
|
* Used by `useBloc` to inherit provider args when no own `args` are given.
|
|
56
|
+
* @public
|
|
55
57
|
*/
|
|
56
58
|
function useProvidedArgs(BlocClass) {
|
|
57
59
|
return (0, react.useContext)(ProvidedArgsContext).get(BlocClass);
|
|
58
60
|
}
|
|
59
61
|
//#endregion
|
|
62
|
+
//#region src/RegistryProvider.tsx
|
|
63
|
+
/**
|
|
64
|
+
* Internal context value: a registry to use in place of the module-global
|
|
65
|
+
* default (`getRegistry()`), or `null` when no provider is present.
|
|
66
|
+
*/
|
|
67
|
+
const RegistryContext = (0, react.createContext)(null);
|
|
68
|
+
/**
|
|
69
|
+
* Scopes descendant `useBloc` calls to a specific registry instead of the
|
|
70
|
+
* module-global default.
|
|
71
|
+
*
|
|
72
|
+
* Useful for test isolation, SSR isolation, and micro-frontends that must
|
|
73
|
+
* not share bloc instances with the host page.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* <RegistryProvider registry={new StateContainerRegistry()}>
|
|
78
|
+
* <App />
|
|
79
|
+
* </RegistryProvider>
|
|
80
|
+
* ```
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
function RegistryProvider({ registry, children }) {
|
|
84
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RegistryContext.Provider, {
|
|
85
|
+
value: registry,
|
|
86
|
+
children
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
60
90
|
//#region src/buildTrackedProxy.ts
|
|
91
|
+
function supportsTrackedState(value) {
|
|
92
|
+
return typeof value[_blac_core.WITH_TRACKED_STATE] === "function";
|
|
93
|
+
}
|
|
94
|
+
const gettersByProto = /* @__PURE__ */ new WeakMap();
|
|
95
|
+
function collectGetters(proto) {
|
|
96
|
+
const cached = gettersByProto.get(proto);
|
|
97
|
+
if (cached !== void 0) return cached;
|
|
98
|
+
const getters = /* @__PURE__ */ new Map();
|
|
99
|
+
let p = proto;
|
|
100
|
+
while (p !== null && p !== Object.prototype) {
|
|
101
|
+
for (const key of Reflect.ownKeys(p)) {
|
|
102
|
+
const desc = Object.getOwnPropertyDescriptor(p, key);
|
|
103
|
+
if (desc?.get && !getters.has(key)) getters.set(key, desc.get);
|
|
104
|
+
}
|
|
105
|
+
p = Object.getPrototypeOf(p);
|
|
106
|
+
}
|
|
107
|
+
gettersByProto.set(proto, getters);
|
|
108
|
+
return getters;
|
|
109
|
+
}
|
|
61
110
|
/**
|
|
62
|
-
* Build a per-consumer proxy
|
|
111
|
+
* Build a per-consumer proxy for a bloc instance.
|
|
63
112
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* - `thisProxy` — the inner `this`-proxy used as the receiver for getter
|
|
70
|
-
* calls. Intercepts `state` to return `trackedStateRef.current` when a
|
|
71
|
-
* tracking context is active; otherwise falls through to the live value.
|
|
72
|
-
* When `onDepHandle` is supplied, a read off `this` whose value carries the
|
|
73
|
-
* `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through
|
|
74
|
-
* `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts
|
|
75
|
-
* the consumer into cross-bloc reactivity. Core stays decoupled — detection
|
|
76
|
-
* is purely by the `DEP_BRAND` symbol.
|
|
113
|
+
* Getters run with `this` bound to the **real instance**, not a proxy, so ES
|
|
114
|
+
* `#private` fields and methods work in user blocs. Path recording is done by
|
|
115
|
+
* `[WITH_TRACKED_STATE]`, which makes the instance's own `state` getter report
|
|
116
|
+
* the render's tracking proxy for the duration of the call; nested getter
|
|
117
|
+
* chains stay tracked because the override is still set.
|
|
77
118
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
119
|
+
* One allocation per bloc acquisition (inside `useMemo`), so the proxy is
|
|
120
|
+
* stable across renders.
|
|
80
121
|
*
|
|
81
|
-
* @param onDepHandle - Optional callback invoked when a
|
|
82
|
-
* dep handle
|
|
83
|
-
*
|
|
84
|
-
*
|
|
122
|
+
* @param onDepHandle - Optional callback invoked when a read returns a branded
|
|
123
|
+
* dep handle. Receives the original handle and returns the value to expose in
|
|
124
|
+
* its place (the session-bound wrapper). The callback is responsible for
|
|
125
|
+
* caching wrappers per handle to avoid re-allocation.
|
|
85
126
|
*/
|
|
86
127
|
function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
for (const key of keys) {
|
|
92
|
-
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
93
|
-
if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);
|
|
94
|
-
}
|
|
95
|
-
proto = Object.getPrototypeOf(proto);
|
|
96
|
-
}
|
|
97
|
-
const thisProxy = new Proxy(instance, { get(t, k, r) {
|
|
98
|
-
if (k === "state") return trackedStateRef.current ?? Reflect.get(t, k, r);
|
|
99
|
-
const value = Reflect.get(t, k, r);
|
|
128
|
+
const getters = collectGetters(Object.getPrototypeOf(instance));
|
|
129
|
+
const tracksState = supportsTrackedState(instance);
|
|
130
|
+
const boundMethods = /* @__PURE__ */ new Map();
|
|
131
|
+
const wrapDepHandle = (value) => {
|
|
100
132
|
if (onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[_blac_core.DEP_BRAND] !== void 0) return onDepHandle(value);
|
|
101
133
|
return value;
|
|
102
|
-
} });
|
|
103
|
-
return {
|
|
104
|
-
proxy: new Proxy(instance, { get(target, key, receiver) {
|
|
105
|
-
const desc = getterDescs.get(key);
|
|
106
|
-
if (desc?.get) return desc.get.call(thisProxy);
|
|
107
|
-
return Reflect.get(target, key, receiver);
|
|
108
|
-
} }),
|
|
109
|
-
thisProxy
|
|
110
134
|
};
|
|
135
|
+
return { proxy: new Proxy(instance, { get(target, key) {
|
|
136
|
+
const getter = getters.get(key);
|
|
137
|
+
if (getter !== void 0) {
|
|
138
|
+
const tracked = trackedStateRef.current;
|
|
139
|
+
return wrapDepHandle(tracked == null || !tracksState ? getter.call(target) : target[_blac_core.WITH_TRACKED_STATE](tracked, getter, onDepHandle));
|
|
140
|
+
}
|
|
141
|
+
const value = Reflect.get(target, key, target);
|
|
142
|
+
if (typeof value === "function") {
|
|
143
|
+
let bound = boundMethods.get(key);
|
|
144
|
+
if (bound === void 0) {
|
|
145
|
+
bound = value.bind(target);
|
|
146
|
+
boundMethods.set(key, bound);
|
|
147
|
+
}
|
|
148
|
+
return wrapDepHandle(bound);
|
|
149
|
+
}
|
|
150
|
+
return wrapDepHandle(value);
|
|
151
|
+
} }) };
|
|
111
152
|
}
|
|
112
153
|
//#endregion
|
|
113
154
|
//#region src/useBloc.ts
|
|
114
155
|
let nextConsumerId = 0;
|
|
115
156
|
const ARGS_UNSET = Symbol("blac.argsKeyUnset");
|
|
116
|
-
const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
|
|
117
|
-
const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
118
157
|
/**
|
|
119
158
|
* React hook that connects a component to a state container with automatic
|
|
120
159
|
* re-render on state changes.
|
|
@@ -123,7 +162,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
|
123
162
|
* - **Auto-tracking** (default): the returned state value is a proxy that
|
|
124
163
|
* records read paths during render. The component re-renders when any
|
|
125
164
|
* recorded path changes. Backed by `@dirtytalk/structural`'s
|
|
126
|
-
*
|
|
165
|
+
* `trackRender` + the container's path-scoped `DirtyChannel`.
|
|
127
166
|
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
128
167
|
* The hook re-renders only when the returned array's elements change
|
|
129
168
|
* (per-index `Object.is`).
|
|
@@ -143,7 +182,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
|
143
182
|
* const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });
|
|
144
183
|
* ```
|
|
145
184
|
*
|
|
146
|
-
* @
|
|
185
|
+
* @typeParam T - The state container constructor type (inferred from BlocClass)
|
|
147
186
|
* @param BlocClass - The state container class to connect to
|
|
148
187
|
* @param options - Configuration options
|
|
149
188
|
* @returns Tuple of `[state, bloc, ref]`
|
|
@@ -164,68 +203,49 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
|
164
203
|
* ```ts
|
|
165
204
|
* const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });
|
|
166
205
|
* ```
|
|
206
|
+
* @public
|
|
167
207
|
*/
|
|
168
208
|
function useBloc(BlocClass, options) {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
const consumerId =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const onMountRef = (0, react.useRef)(options?.onMount);
|
|
177
|
-
onMountRef.current = options?.onMount;
|
|
178
|
-
const onUnmountRef = (0, react.useRef)(options?.onUnmount);
|
|
179
|
-
onUnmountRef.current = options?.onUnmount;
|
|
209
|
+
const registry = (0, react.useContext)(RegistryContext) ?? (0, _blac_core.getRegistry)();
|
|
210
|
+
const consumerRef = (0, react.useRef)(null);
|
|
211
|
+
const consumer = consumerRef.current ??= createConsumer();
|
|
212
|
+
const consumerId = consumer.id;
|
|
213
|
+
consumer.select = options?.select;
|
|
214
|
+
consumer.onMount = options?.onMount;
|
|
215
|
+
consumer.onUnmount = options?.onUnmount;
|
|
180
216
|
const ownArgs = options?.args;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) ownArgsKeyRef.current = {
|
|
188
|
-
ref: ownArgs,
|
|
189
|
-
key: ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs)
|
|
190
|
-
};
|
|
191
|
-
const ownArgsKey = ownArgsKeyRef.current.key;
|
|
217
|
+
consumer.ownArgs = ownArgs;
|
|
218
|
+
if (!Object.is(consumer.ownArgsKeyFor, ownArgs)) {
|
|
219
|
+
consumer.ownArgsKeyFor = ownArgs;
|
|
220
|
+
consumer.ownArgsKey = ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs);
|
|
221
|
+
}
|
|
222
|
+
const ownArgsKey = consumer.ownArgsKey;
|
|
192
223
|
const providerArgs = useProvidedArgs(BlocClass);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
ref: providerArgs,
|
|
201
|
-
key: providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
|
|
202
|
-
};
|
|
203
|
-
const providerArgsKey = providerArgsKeyRef.current.key;
|
|
204
|
-
const trackedStateRef = (0, react.useRef)(null);
|
|
205
|
-
const sessionRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
206
|
-
const depSubsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
207
|
-
const depWrapperCacheRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
208
|
-
const proxyCacheRef = (0, react.useRef)(new _dirtytalk_structural.ProxyCache());
|
|
209
|
-
const lastReconcileRef = (0, react.useRef)(null);
|
|
210
|
-
const [rebindNonce, bumpRebind] = (0, react.useReducer)((x) => x + 1, 0);
|
|
211
|
-
const ownedBlocRef = (0, react.useRef)(null);
|
|
224
|
+
consumer.providerArgs = providerArgs;
|
|
225
|
+
if (!Object.is(consumer.providerArgsKeyFor, providerArgs)) {
|
|
226
|
+
consumer.providerArgsKeyFor = providerArgs;
|
|
227
|
+
consumer.providerArgsKey = providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs);
|
|
228
|
+
}
|
|
229
|
+
const providerArgsKey = consumer.providerArgsKey;
|
|
230
|
+
const rebindNonce = consumer.rebindNonce;
|
|
212
231
|
const { bloc, instanceKey, trackedBloc } = (0, react.useMemo)(() => {
|
|
213
|
-
const effectiveArgs =
|
|
232
|
+
const effectiveArgs = resolveEffectiveArgs(consumer);
|
|
214
233
|
const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, effectiveArgs);
|
|
215
|
-
const instance =
|
|
234
|
+
const instance = registry.acquire(BlocClass, resolvedKey, {
|
|
216
235
|
canCreate: true,
|
|
217
236
|
countRef: false,
|
|
218
|
-
args: effectiveArgs
|
|
237
|
+
args: effectiveArgs,
|
|
238
|
+
sweepIfUnowned: true
|
|
219
239
|
});
|
|
220
240
|
const onDepHandle = (handle) => {
|
|
221
|
-
const cache =
|
|
241
|
+
const cache = consumer.depWrappers ??= /* @__PURE__ */ new Map();
|
|
222
242
|
const cached = cache.get(handle);
|
|
223
243
|
if (cached !== void 0) return cached;
|
|
224
|
-
const wrapper = makeDepWrapper(handle,
|
|
244
|
+
const wrapper = makeDepWrapper(handle, registry, consumer, onDepHandle);
|
|
225
245
|
cache.set(handle, wrapper);
|
|
226
246
|
return wrapper;
|
|
227
247
|
};
|
|
228
|
-
const { proxy } = buildTrackedProxy(instance,
|
|
248
|
+
const { proxy } = buildTrackedProxy(instance, consumer.tracked, onDepHandle);
|
|
229
249
|
return {
|
|
230
250
|
bloc: instance,
|
|
231
251
|
instanceKey: resolvedKey,
|
|
@@ -235,101 +255,98 @@ function useBloc(BlocClass, options) {
|
|
|
235
255
|
BlocClass,
|
|
236
256
|
ownArgsKey,
|
|
237
257
|
providerArgsKey,
|
|
238
|
-
rebindNonce
|
|
258
|
+
rebindNonce,
|
|
259
|
+
registry
|
|
239
260
|
]);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
force();
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
const next = select(bloc.state, bloc);
|
|
256
|
-
const prev = lastSelectionRef.current;
|
|
257
|
-
if (prev !== null && shallowArrayEqual(prev, next)) return;
|
|
258
|
-
lastSelectionRef.current = next;
|
|
259
|
-
force();
|
|
260
|
-
});
|
|
261
|
-
const select = selectRef.current;
|
|
262
|
-
if (select) {
|
|
263
|
-
const next = select(bloc.state, bloc);
|
|
264
|
-
const prev = lastSelectionRef.current;
|
|
265
|
-
if (prev === null || !shallowArrayEqual(prev, next)) {
|
|
266
|
-
lastSelectionRef.current = next;
|
|
267
|
-
force();
|
|
261
|
+
consumer.container = bloc;
|
|
262
|
+
(0, react.useSyncExternalStore)((0, react.useMemo)(() => (onStoreChange) => {
|
|
263
|
+
consumer.notify = onStoreChange;
|
|
264
|
+
const container = consumer.container;
|
|
265
|
+
const unsubscribe = container.channel.subscribe(() => consumer.interest, () => {
|
|
266
|
+
if (consumer.isSelectMode) {
|
|
267
|
+
const select = consumer.select;
|
|
268
|
+
if (select) {
|
|
269
|
+
const next = select(container.state, container);
|
|
270
|
+
const prev = consumer.selection;
|
|
271
|
+
if (prev !== null && shallowArrayEqual(prev, next)) return;
|
|
272
|
+
consumer.selection = next;
|
|
268
273
|
}
|
|
269
274
|
}
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
bloc.registerConsumerPaths(consumerId, pathRef.current);
|
|
274
|
-
if (bloc.state !== renderStateRef.current) force();
|
|
275
|
+
consumer.bump();
|
|
276
|
+
});
|
|
277
|
+
container.registerConsumerPaths(consumerId, consumer.paths);
|
|
275
278
|
return () => {
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
consumer.notify = noop;
|
|
280
|
+
unsubscribe();
|
|
281
|
+
container.unregisterConsumer(consumerId);
|
|
278
282
|
};
|
|
279
|
-
}, [
|
|
283
|
+
}, [
|
|
284
|
+
BlocClass,
|
|
285
|
+
instanceKey,
|
|
286
|
+
consumerId,
|
|
287
|
+
rebindNonce
|
|
288
|
+
]), consumer.getSnapshot, consumer.getServerSnapshot);
|
|
280
289
|
(0, react.useLayoutEffect)(() => {
|
|
281
|
-
const registry = (0, _blac_core.getRegistry)();
|
|
282
290
|
const live = registry.acquire(BlocClass, instanceKey, {
|
|
283
291
|
canCreate: true,
|
|
284
292
|
countRef: true,
|
|
285
|
-
refId: primaryRefId
|
|
293
|
+
refId: consumer.primaryRefId,
|
|
294
|
+
args: resolveEffectiveArgs(consumer)
|
|
286
295
|
});
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (live !== bloc)
|
|
296
|
+
consumer.ownedBloc = live;
|
|
297
|
+
consumer.onMount?.(live);
|
|
298
|
+
if (live !== bloc) {
|
|
299
|
+
consumer.rebindNonce++;
|
|
300
|
+
consumer.bump();
|
|
301
|
+
}
|
|
290
302
|
return () => {
|
|
291
|
-
|
|
292
|
-
registry.release(BlocClass, instanceKey, false, primaryRefId
|
|
303
|
+
consumer.onUnmount?.(consumer.ownedBloc ?? bloc);
|
|
304
|
+
registry.release(BlocClass, instanceKey, false, consumer.primaryRefId);
|
|
293
305
|
};
|
|
294
306
|
}, [
|
|
295
307
|
BlocClass,
|
|
296
308
|
instanceKey,
|
|
297
|
-
consumerId
|
|
309
|
+
consumerId,
|
|
310
|
+
registry
|
|
298
311
|
]);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
lastSelectionRef.current = null;
|
|
304
|
-
}
|
|
312
|
+
const container = consumer.container;
|
|
313
|
+
const rawState = container.state;
|
|
314
|
+
const select = consumer.select;
|
|
315
|
+
consumer.isSelectMode = select !== void 0;
|
|
305
316
|
let state;
|
|
306
|
-
if (
|
|
317
|
+
if (select !== void 0) {
|
|
307
318
|
state = rawState;
|
|
308
|
-
if (
|
|
319
|
+
if (consumer.selection === null) consumer.selection = select(rawState, bloc);
|
|
320
|
+
consumer.interest = _dirtytalk_structural.ALL_PATHS;
|
|
309
321
|
} else {
|
|
310
|
-
const tracked = (0, _dirtytalk_structural.trackRender)(rawState,
|
|
322
|
+
const tracked = (0, _dirtytalk_structural.trackRender)(rawState, container.interner, consumer.proxyCache ??= new _dirtytalk_structural.ProxyCache());
|
|
311
323
|
state = tracked.value;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
324
|
+
consumer.tracked.current = tracked.value;
|
|
325
|
+
consumer.paths = tracked.paths;
|
|
326
|
+
consumer.disarm = tracked.disarm;
|
|
327
|
+
const session = consumer.session;
|
|
315
328
|
session.clear();
|
|
316
|
-
session.set(
|
|
329
|
+
session.set(container, {
|
|
317
330
|
kind: "primary",
|
|
318
331
|
paths: tracked.paths
|
|
319
332
|
});
|
|
320
|
-
queueMicrotask(tracked.disarm);
|
|
321
333
|
}
|
|
322
334
|
(0, react.useLayoutEffect)(() => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
consumer.tracked.current = null;
|
|
336
|
+
const disarm = consumer.disarm;
|
|
337
|
+
if (disarm !== null) {
|
|
338
|
+
consumer.disarm = null;
|
|
339
|
+
disarm();
|
|
340
|
+
}
|
|
341
|
+
if (consumer.isSelectMode) {
|
|
342
|
+
consumer.lastReconcile = null;
|
|
326
343
|
return;
|
|
327
344
|
}
|
|
328
|
-
const
|
|
329
|
-
const paths =
|
|
330
|
-
const session =
|
|
331
|
-
const last =
|
|
332
|
-
if (last !== null && last.primaryContainer ===
|
|
345
|
+
const primary = consumer.container;
|
|
346
|
+
const paths = consumer.paths;
|
|
347
|
+
const session = consumer.session;
|
|
348
|
+
const last = consumer.lastReconcile;
|
|
349
|
+
if (last !== null && last.primaryContainer === primary && (0, _dirtytalk_structural.pathSetEquals)(last.primaryPaths, paths)) {
|
|
333
350
|
let unchanged = last.deps.size === session.size - 1;
|
|
334
351
|
if (unchanged) for (const [depContainer, entry] of session) {
|
|
335
352
|
if (entry.kind === "primary") continue;
|
|
@@ -341,13 +358,13 @@ function useBloc(BlocClass, options) {
|
|
|
341
358
|
}
|
|
342
359
|
if (unchanged) return;
|
|
343
360
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const subs =
|
|
361
|
+
primary.registerConsumerPaths(consumerId, paths);
|
|
362
|
+
consumer.interest = expandWithAncestors(paths, primary.interner);
|
|
363
|
+
const subs = consumer.depSubs;
|
|
347
364
|
for (const [depContainer, sub] of subs) if (!session.has(depContainer)) {
|
|
348
365
|
sub.unsubscribe();
|
|
349
366
|
depContainer.unregisterConsumer(consumerId);
|
|
350
|
-
|
|
367
|
+
registry.release(sub.Type, sub.key, false, sub.refId);
|
|
351
368
|
subs.delete(depContainer);
|
|
352
369
|
}
|
|
353
370
|
for (const [depContainer, entry] of session) {
|
|
@@ -359,14 +376,14 @@ function useBloc(BlocClass, options) {
|
|
|
359
376
|
existing.interestRef.current = interest;
|
|
360
377
|
continue;
|
|
361
378
|
}
|
|
362
|
-
|
|
379
|
+
registry.acquire(entry.Type, entry.key, {
|
|
363
380
|
canCreate: true,
|
|
364
381
|
countRef: true,
|
|
365
382
|
refId: entry.refId,
|
|
366
383
|
args: entry.args
|
|
367
384
|
});
|
|
368
385
|
const interestRef = { current: interest };
|
|
369
|
-
const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () =>
|
|
386
|
+
const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => consumer.bump());
|
|
370
387
|
subs.set(depContainer, {
|
|
371
388
|
unsubscribe,
|
|
372
389
|
interestRef,
|
|
@@ -386,35 +403,73 @@ function useBloc(BlocClass, options) {
|
|
|
386
403
|
args: entry.args
|
|
387
404
|
});
|
|
388
405
|
}
|
|
389
|
-
|
|
390
|
-
primaryContainer:
|
|
406
|
+
consumer.lastReconcile = {
|
|
407
|
+
primaryContainer: primary,
|
|
391
408
|
primaryPaths: paths,
|
|
392
409
|
deps: depsSignature
|
|
393
410
|
};
|
|
394
411
|
});
|
|
395
412
|
(0, react.useEffect)(() => {
|
|
396
|
-
const subs =
|
|
413
|
+
const subs = consumer.depSubs;
|
|
397
414
|
return () => {
|
|
398
415
|
for (const [depContainer, sub] of subs) {
|
|
399
416
|
sub.unsubscribe();
|
|
400
417
|
depContainer.unregisterConsumer(consumerId);
|
|
401
|
-
|
|
418
|
+
registry.release(sub.Type, sub.key, false, sub.refId);
|
|
402
419
|
}
|
|
403
420
|
subs.clear();
|
|
404
421
|
};
|
|
405
|
-
}, [consumerId]);
|
|
406
|
-
return [
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
422
|
+
}, [consumerId, registry]);
|
|
423
|
+
return [state, trackedBloc];
|
|
424
|
+
}
|
|
425
|
+
const noop = () => {};
|
|
426
|
+
function createConsumer() {
|
|
427
|
+
const id = `useBloc-${nextConsumerId++}`;
|
|
428
|
+
const consumer = {
|
|
429
|
+
id,
|
|
430
|
+
primaryRefId: `useBloc@${id}`,
|
|
431
|
+
depRefId: `useBloc@${id}:dep`,
|
|
432
|
+
container: null,
|
|
433
|
+
version: 0,
|
|
434
|
+
notify: noop,
|
|
435
|
+
bump: () => {
|
|
436
|
+
consumer.version++;
|
|
437
|
+
consumer.notify();
|
|
438
|
+
},
|
|
439
|
+
getSnapshot: () => consumer.version,
|
|
440
|
+
getServerSnapshot: () => consumer.version,
|
|
441
|
+
interest: (0, _dirtytalk_structural.emptyPathSet)(),
|
|
442
|
+
paths: (0, _dirtytalk_structural.emptyPathSet)(),
|
|
443
|
+
isSelectMode: false,
|
|
444
|
+
selection: null,
|
|
445
|
+
session: /* @__PURE__ */ new Map(),
|
|
446
|
+
depSubs: /* @__PURE__ */ new Map(),
|
|
447
|
+
lastReconcile: null,
|
|
448
|
+
select: void 0,
|
|
449
|
+
onMount: void 0,
|
|
450
|
+
onUnmount: void 0,
|
|
451
|
+
ownArgs: void 0,
|
|
452
|
+
ownArgsKeyFor: ARGS_UNSET,
|
|
453
|
+
ownArgsKey: void 0,
|
|
454
|
+
providerArgs: void 0,
|
|
455
|
+
providerArgsKeyFor: ARGS_UNSET,
|
|
456
|
+
providerArgsKey: void 0,
|
|
457
|
+
tracked: { current: null },
|
|
458
|
+
disarm: null,
|
|
459
|
+
proxyCache: null,
|
|
460
|
+
depWrappers: null,
|
|
461
|
+
rebindNonce: 0,
|
|
462
|
+
ownedBloc: null
|
|
463
|
+
};
|
|
464
|
+
return consumer;
|
|
411
465
|
}
|
|
466
|
+
const resolveEffectiveArgs = (consumer) => consumer.ownArgs !== void 0 ? consumer.ownArgs : consumer.providerArgs;
|
|
412
467
|
/**
|
|
413
468
|
* Build the per-consumer wrapper that replaces a branded dep handle inside a
|
|
414
469
|
* tracked getter's `this`. The wrapper exposes the same accessors as the core
|
|
415
470
|
* handle and overrides `.track()`:
|
|
416
471
|
*
|
|
417
|
-
* - **Inside a render** (`
|
|
472
|
+
* - **Inside a render** (`consumer.tracked.current != null`): resolve (ENSURE,
|
|
418
473
|
* no ref) the dep, `trackRender` its state, merge the recorded paths into the
|
|
419
474
|
* session entry, build/reuse a tracked proxy for the dep so its OWN getters
|
|
420
475
|
* track too, and return `[trackedValue, depProxy]`. The ownership ref is taken
|
|
@@ -431,10 +486,9 @@ function useBloc(BlocClass, options) {
|
|
|
431
486
|
* the dep already has a non-primary session entry this render, reuse its proxy
|
|
432
487
|
* + union its paths instead of re-acquiring.
|
|
433
488
|
*/
|
|
434
|
-
function makeDepWrapper(handle,
|
|
489
|
+
function makeDepWrapper(handle, registry, consumer, onDepHandle) {
|
|
435
490
|
const brand = handle[_blac_core.DEP_BRAND];
|
|
436
|
-
const refId = depRefId
|
|
437
|
-
const registry = (0, _blac_core.getRegistry)();
|
|
491
|
+
const refId = consumer.depRefId;
|
|
438
492
|
const perDep = /* @__PURE__ */ new Map();
|
|
439
493
|
const proxyCache = new _dirtytalk_structural.ProxyCache();
|
|
440
494
|
const resolve = (options) => {
|
|
@@ -450,8 +504,8 @@ function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHa
|
|
|
450
504
|
untracked: (options) => resolve(options).dep,
|
|
451
505
|
track: (options) => {
|
|
452
506
|
const { dep, key, args } = resolve(options);
|
|
453
|
-
if (
|
|
454
|
-
const session =
|
|
507
|
+
if (consumer.tracked.current == null) return [dep.state, dep];
|
|
508
|
+
const session = consumer.session;
|
|
455
509
|
const existing = session.get(dep);
|
|
456
510
|
const tracked = (0, _dirtytalk_structural.trackRender)(dep.state, dep.interner, proxyCache);
|
|
457
511
|
let cache = perDep.get(dep);
|
|
@@ -507,7 +561,7 @@ const shallowArrayEqual = (a, b) => {
|
|
|
507
561
|
* would miss a `patch`-triggered atomic-replacement of `items`.
|
|
508
562
|
*
|
|
509
563
|
* Ancestors are added under the interner's *ancestor-watch* lane
|
|
510
|
-
* (`
|
|
564
|
+
* (`ancestorWatchIds`), NOT as normal ids. The source emits a matching
|
|
511
565
|
* ancestor-watch mark only for paths it replaces atomically — never for a
|
|
512
566
|
* plain-object structural pulse-up. So `{'items.length'}` wakes when the array
|
|
513
567
|
* `items` is replaced, but `{'user.email'}` does NOT wake when a sibling
|
|
@@ -517,38 +571,57 @@ const shallowArrayEqual = (a, b) => {
|
|
|
517
571
|
* Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but
|
|
518
572
|
* NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,
|
|
519
573
|
* and `''` would wake this consumer on every field change).
|
|
574
|
+
*
|
|
575
|
+
* Returns `paths` itself when no leaf has an ancestor (top-level fields only);
|
|
576
|
+
* the set is frozen by then (see `Consumer.disarm`), so sharing it is safe.
|
|
520
577
|
*/
|
|
521
578
|
function expandWithAncestors(paths, interner) {
|
|
522
579
|
if (paths === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
|
|
523
580
|
const leafPaths = paths;
|
|
524
|
-
|
|
525
|
-
const expanded = new Set(leafPaths);
|
|
581
|
+
let expanded;
|
|
526
582
|
for (const id of leafPaths) {
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
expanded.add(interner.internAncestor(ancestor));
|
|
532
|
-
idx = ancestor.lastIndexOf(".");
|
|
533
|
-
}
|
|
583
|
+
const watch = interner.ancestorWatchIds(id);
|
|
584
|
+
if (watch.length === 0) continue;
|
|
585
|
+
expanded ??= new Set(leafPaths);
|
|
586
|
+
for (let i = 0; i < watch.length; i++) expanded.add(watch[i]);
|
|
534
587
|
}
|
|
535
|
-
return expanded;
|
|
588
|
+
return expanded ?? paths;
|
|
536
589
|
}
|
|
537
|
-
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/useBlocDeps.ts
|
|
538
592
|
/**
|
|
539
|
-
*
|
|
593
|
+
* Feed live values from the component into a bloc's `deps` lane.
|
|
594
|
+
*
|
|
595
|
+
* Each calling component is one owner: its slice is shallow-merged into the
|
|
596
|
+
* bloc's merged `deps` view, and withdrawn when the component unmounts. The
|
|
597
|
+
* bloc observes changes through `onDepsChanged`.
|
|
598
|
+
*
|
|
599
|
+
* ```tsx
|
|
600
|
+
* const [state, bloc] = useBloc(CanvasCubit);
|
|
601
|
+
* useBlocDeps(bloc, { canvas, onTick });
|
|
602
|
+
* ```
|
|
540
603
|
*
|
|
541
|
-
*
|
|
604
|
+
* Use this for values a bloc must *read* but should not own — DOM handles,
|
|
605
|
+
* callbacks, values from other hooks. Pass identity-stable values (`useMemo` /
|
|
606
|
+
* `useCallback`) as you would to any hook; a new function identity each render
|
|
607
|
+
* counts as a change.
|
|
542
608
|
*/
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
609
|
+
function useBlocDeps(bloc, slice) {
|
|
610
|
+
const ownerId = (0, react.useId)();
|
|
611
|
+
const latest = (0, react.useRef)(slice);
|
|
612
|
+
latest.current = slice;
|
|
613
|
+
(0, react.useEffect)(() => {
|
|
614
|
+
bloc[_blac_core.APPLY_DEPS](ownerId, latest.current);
|
|
615
|
+
});
|
|
616
|
+
(0, react.useEffect)(() => {
|
|
617
|
+
return () => {
|
|
618
|
+
bloc[_blac_core.REMOVE_DEPS_OWNER](ownerId);
|
|
619
|
+
};
|
|
620
|
+
}, [bloc, ownerId]);
|
|
548
621
|
}
|
|
549
622
|
//#endregion
|
|
550
623
|
exports.BlocProvider = BlocProvider;
|
|
551
|
-
exports.
|
|
624
|
+
exports.RegistryProvider = RegistryProvider;
|
|
552
625
|
Object.defineProperty(exports, "untracked", {
|
|
553
626
|
enumerable: true,
|
|
554
627
|
get: function() {
|
|
@@ -556,6 +629,7 @@ Object.defineProperty(exports, "untracked", {
|
|
|
556
629
|
}
|
|
557
630
|
});
|
|
558
631
|
exports.useBloc = useBloc;
|
|
632
|
+
exports.useBlocDeps = useBlocDeps;
|
|
559
633
|
exports.useProvidedArgs = useProvidedArgs;
|
|
560
634
|
|
|
561
635
|
//# sourceMappingURL=index.cjs.map
|