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