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