mobx-mantle 0.3.7 → 0.7.0
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 +1558 -722
- package/dist/behaviors/index.cjs +419 -0
- package/dist/behaviors/index.cjs.map +1 -0
- package/dist/behaviors/index.d.cts +275 -0
- package/dist/behaviors/index.d.ts +275 -0
- package/dist/behaviors/index.js +419 -0
- package/dist/behaviors/index.js.map +1 -0
- package/dist/chunk-LP5TAGSI.js +9 -0
- package/dist/chunk-LP5TAGSI.js.map +1 -0
- package/dist/chunk-WNF647U6.cjs +9 -0
- package/dist/chunk-WNF647U6.cjs.map +1 -0
- package/dist/index.cjs +1237 -256
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +468 -83
- package/dist/index.d.ts +468 -83
- package/dist/index.js +1223 -197
- package/dist/index.js.map +1 -1
- package/dist/reactive-args-BBucrU54.d.cts +32 -0
- package/dist/reactive-args-BBucrU54.d.ts +32 -0
- package/dist/testing/index.cjs +33 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +18 -0
- package/dist/testing/index.d.ts +18 -0
- package/dist/testing/index.js +33 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +42 -8
package/dist/index.js
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { useObserver } from "mobx-react-lite";
|
|
1
|
+
import {
|
|
2
|
+
toValue
|
|
3
|
+
} from "./chunk-LP5TAGSI.js";
|
|
5
4
|
|
|
6
|
-
// src/
|
|
7
|
-
import
|
|
5
|
+
// src/services.tsx
|
|
6
|
+
import React from "react";
|
|
8
7
|
|
|
9
8
|
// src/config.ts
|
|
9
|
+
import { configure as mobxConfigure } from "mobx";
|
|
10
10
|
var globalConfig = {
|
|
11
|
-
autoObservable: true
|
|
11
|
+
autoObservable: true,
|
|
12
|
+
cacheAnnotations: true,
|
|
13
|
+
manageMobxActions: false
|
|
12
14
|
};
|
|
15
|
+
var actionPolicyApplied = false;
|
|
16
|
+
function applyMobxActionPolicy() {
|
|
17
|
+
if (actionPolicyApplied) return;
|
|
18
|
+
actionPolicyApplied = true;
|
|
19
|
+
if (globalConfig.manageMobxActions === true) {
|
|
20
|
+
mobxConfigure({ enforceActions: "never" });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
function reportError(error, context) {
|
|
14
24
|
if (globalConfig.onError) {
|
|
15
25
|
globalConfig.onError(error, context);
|
|
16
26
|
} else {
|
|
17
27
|
console.error(
|
|
18
|
-
`[mobx-mantle] Error in ${context.isBehavior ? "behavior" : "
|
|
28
|
+
`[mobx-mantle] Error in ${context.isBehavior ? "behavior" : "component"} ${context.name}.${context.phase}():`,
|
|
19
29
|
error
|
|
20
30
|
);
|
|
21
31
|
}
|
|
@@ -24,18 +34,355 @@ function configure(config) {
|
|
|
24
34
|
Object.assign(globalConfig, config);
|
|
25
35
|
}
|
|
26
36
|
|
|
37
|
+
// src/services.tsx
|
|
38
|
+
import { jsx } from "react/jsx-runtime";
|
|
39
|
+
function createServiceToken(description) {
|
|
40
|
+
return Symbol(description);
|
|
41
|
+
}
|
|
42
|
+
var ServiceContext = React.createContext(void 0);
|
|
43
|
+
function ServiceProvider({ resolve, children }) {
|
|
44
|
+
return /* @__PURE__ */ jsx(ServiceContext.Provider, { value: resolve, children });
|
|
45
|
+
}
|
|
46
|
+
var constructingScope;
|
|
47
|
+
var scopes = /* @__PURE__ */ new WeakMap();
|
|
48
|
+
function runWithScope(scope, construct) {
|
|
49
|
+
const previous = constructingScope;
|
|
50
|
+
constructingScope = scope;
|
|
51
|
+
try {
|
|
52
|
+
return construct();
|
|
53
|
+
} finally {
|
|
54
|
+
constructingScope = previous;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function withInjectionScope(resolve, construct) {
|
|
58
|
+
return runWithScope({ resolve: resolve ?? globalConfig.resolveService, values: /* @__PURE__ */ new WeakSet() }, construct);
|
|
59
|
+
}
|
|
60
|
+
function captureServiceScope(owner) {
|
|
61
|
+
scopes.set(owner, constructingScope ?? { resolve: globalConfig.resolveService, values: /* @__PURE__ */ new WeakSet() });
|
|
62
|
+
}
|
|
63
|
+
function getServiceResolver(owner) {
|
|
64
|
+
return scopes.get(owner)?.resolve;
|
|
65
|
+
}
|
|
66
|
+
function resolveService(owner, token) {
|
|
67
|
+
return resolveInScope(scopes.get(owner), token);
|
|
68
|
+
}
|
|
69
|
+
function inject(token) {
|
|
70
|
+
if (!constructingScope) {
|
|
71
|
+
throw new Error("[mobx-mantle] inject() requires an active construction scope. Use this.inject() in later methods, or withInjectionScope() outside React.");
|
|
72
|
+
}
|
|
73
|
+
return resolveInScope(constructingScope, token);
|
|
74
|
+
}
|
|
75
|
+
function resolveInScope(scope, token) {
|
|
76
|
+
if (!scope?.resolve) throw new Error("[mobx-mantle] Service injection requires configure({ resolveService }), a ServiceProvider, or withInjectionScope() during construction.");
|
|
77
|
+
const value = scope.resolve(token);
|
|
78
|
+
if ((typeof value !== "object" || value === null) && typeof value !== "function") {
|
|
79
|
+
throw new Error("[mobx-mantle] Services must be objects or functions; wrap primitive configuration in an object.");
|
|
80
|
+
}
|
|
81
|
+
scope.values.add(value);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function isServiceValue(owner, value) {
|
|
85
|
+
return scopes.get(owner)?.values.has(value) ?? false;
|
|
86
|
+
}
|
|
87
|
+
function withOwnerServiceScope(owner, construct) {
|
|
88
|
+
const scope = scopes.get(owner);
|
|
89
|
+
if (!scope) throw new Error("[mobx-mantle] Missing owner service scope.");
|
|
90
|
+
return runWithScope(scope, construct);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/model-scope.tsx
|
|
94
|
+
import React2 from "react";
|
|
95
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
96
|
+
var ModelContext = React2.createContext(void 0);
|
|
97
|
+
function ModelProvider({ models, children }) {
|
|
98
|
+
const [snapshot] = React2.useState(() => new Map(Array.from(models, ([type, replacement]) => [type, { ...replacement }])));
|
|
99
|
+
return /* @__PURE__ */ jsx2(ModelContext.Provider, { value: snapshot, children });
|
|
100
|
+
}
|
|
101
|
+
var identities = /* @__PURE__ */ new WeakMap();
|
|
102
|
+
function setModelIdentity(model, type) {
|
|
103
|
+
identities.set(model, type);
|
|
104
|
+
}
|
|
105
|
+
function isModel(model, type) {
|
|
106
|
+
const logical = identities.get(model);
|
|
107
|
+
return model instanceof type || logical === type || !!logical && logical.prototype instanceof type;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/mantle.tsx
|
|
111
|
+
import React4, { useRef as useRef2, useEffect as useEffect2, useContext, forwardRef as reactForwardRef, memo as memo2 } from "react";
|
|
112
|
+
import { makeObservable as makeObservable2, observable as observable4, computed as computed4, runInAction as runInAction3, reaction as reaction2, autorun as autorun2, createAtom, isObservableProp, _getGlobalState as _getGlobalState2 } from "mobx";
|
|
113
|
+
|
|
114
|
+
// src/observer.ts
|
|
115
|
+
import { memo, useRef, useSyncExternalStore, useEffect, useLayoutEffect } from "react";
|
|
116
|
+
import { Reaction } from "mobx";
|
|
117
|
+
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
118
|
+
var uncommittedReactionRegistry = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((adm) => {
|
|
119
|
+
adm.reaction?.dispose();
|
|
120
|
+
adm.reaction = null;
|
|
121
|
+
}) : void 0;
|
|
122
|
+
if (process.env.NODE_ENV !== "production" && !uncommittedReactionRegistry) {
|
|
123
|
+
console.warn(
|
|
124
|
+
"[mobx-mantle] FinalizationRegistry is not available in this environment. Reactions created for renders that React discards cannot be cleaned up, which can leak under Suspense and concurrent rendering."
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
var isServer = typeof window === "undefined";
|
|
128
|
+
var trackedComponents = /* @__PURE__ */ new WeakSet();
|
|
129
|
+
function createReaction(adm) {
|
|
130
|
+
adm.reaction = new Reaction(`mantle-observer(${adm.name})`, () => {
|
|
131
|
+
adm.stateVersion = /* @__PURE__ */ Symbol();
|
|
132
|
+
adm.onStoreChange?.();
|
|
133
|
+
});
|
|
134
|
+
if (adm.holder) {
|
|
135
|
+
adm.holder.current = adm.reaction;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function useMantleObserver(render, name, holder) {
|
|
139
|
+
if (isServer) {
|
|
140
|
+
return render();
|
|
141
|
+
}
|
|
142
|
+
const admRef = useRef(null);
|
|
143
|
+
if (!admRef.current) {
|
|
144
|
+
const adm2 = {
|
|
145
|
+
reaction: null,
|
|
146
|
+
onStoreChange: null,
|
|
147
|
+
stateVersion: /* @__PURE__ */ Symbol(),
|
|
148
|
+
name,
|
|
149
|
+
holder,
|
|
150
|
+
subscribe(onStoreChange) {
|
|
151
|
+
uncommittedReactionRegistry?.unregister(adm2);
|
|
152
|
+
adm2.onStoreChange = onStoreChange;
|
|
153
|
+
if (!adm2.reaction) {
|
|
154
|
+
createReaction(adm2);
|
|
155
|
+
adm2.stateVersion = /* @__PURE__ */ Symbol();
|
|
156
|
+
onStoreChange();
|
|
157
|
+
}
|
|
158
|
+
return () => {
|
|
159
|
+
adm2.onStoreChange = null;
|
|
160
|
+
adm2.reaction?.dispose();
|
|
161
|
+
adm2.reaction = null;
|
|
162
|
+
if (adm2.holder) {
|
|
163
|
+
adm2.holder.current = null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
getSnapshot() {
|
|
168
|
+
return adm2.stateVersion;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
admRef.current = adm2;
|
|
172
|
+
}
|
|
173
|
+
const adm = admRef.current;
|
|
174
|
+
adm.holder = holder;
|
|
175
|
+
if (!adm.reaction) {
|
|
176
|
+
createReaction(adm);
|
|
177
|
+
uncommittedReactionRegistry?.register(admRef, adm, adm);
|
|
178
|
+
}
|
|
179
|
+
useSyncExternalStore(adm.subscribe, adm.getSnapshot, adm.getSnapshot);
|
|
180
|
+
let result;
|
|
181
|
+
let didThrow = false;
|
|
182
|
+
let thrown;
|
|
183
|
+
adm.reaction.track(() => {
|
|
184
|
+
try {
|
|
185
|
+
result = render();
|
|
186
|
+
} catch (e) {
|
|
187
|
+
didThrow = true;
|
|
188
|
+
thrown = e;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (didThrow) {
|
|
192
|
+
throw thrown;
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
function observer(fc) {
|
|
197
|
+
const name = fc.displayName || fc.name || "observed";
|
|
198
|
+
const Observed = (props) => useMantleObserver(() => fc(props), name);
|
|
199
|
+
Observed.displayName = `observer(${name})`;
|
|
200
|
+
const Memoized = memo(Observed);
|
|
201
|
+
trackedComponents.add(Memoized);
|
|
202
|
+
return Memoized;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/decorators.ts
|
|
206
|
+
import * as mobx from "mobx";
|
|
207
|
+
var ANNOTATIONS = /* @__PURE__ */ Symbol("mantle:annotations");
|
|
208
|
+
var legacy = /* @__PURE__ */ new WeakMap();
|
|
209
|
+
function annotation(value, kind) {
|
|
210
|
+
return function(_target, key, descriptor) {
|
|
211
|
+
let map;
|
|
212
|
+
let name;
|
|
213
|
+
if (typeof key === "object") {
|
|
214
|
+
if (key.static || key.private || key.kind !== kind) {
|
|
215
|
+
throw new Error(`[mobx-mantle] This decorator requires a public instance ${kind}. Use an ordinary field, not an auto-accessor, for @observable.`);
|
|
216
|
+
}
|
|
217
|
+
if (!key.metadata) throw new Error("[mobx-mantle] TC39 decorators require Symbol.metadata. Load its polyfill before decorated classes.");
|
|
218
|
+
if (!Object.prototype.hasOwnProperty.call(key.metadata, ANNOTATIONS)) key.metadata[ANNOTATIONS] = /* @__PURE__ */ Object.create(null);
|
|
219
|
+
map = key.metadata[ANNOTATIONS];
|
|
220
|
+
name = key.name;
|
|
221
|
+
} else {
|
|
222
|
+
if (typeof _target === "function" || kind === "method" && typeof descriptor?.value !== "function" || kind === "getter" && !descriptor?.get || kind === "field" && descriptor) {
|
|
223
|
+
throw new Error(`[mobx-mantle] This decorator requires a public instance ${kind}.`);
|
|
224
|
+
}
|
|
225
|
+
map = legacy.get(_target) ?? /* @__PURE__ */ Object.create(null);
|
|
226
|
+
legacy.set(_target, map);
|
|
227
|
+
name = key;
|
|
228
|
+
}
|
|
229
|
+
map[name] = value;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
var observable2 = Object.assign(annotation(mobx.observable, "field"), {
|
|
233
|
+
ref: annotation(mobx.observable.ref, "field"),
|
|
234
|
+
shallow: annotation(mobx.observable.shallow, "field"),
|
|
235
|
+
deep: annotation(mobx.observable.deep, "field"),
|
|
236
|
+
struct: annotation(mobx.observable.struct, "field")
|
|
237
|
+
});
|
|
238
|
+
var actionMethod = annotation(mobx.action.bound, "method");
|
|
239
|
+
var actionField = annotation(mobx.action, "field");
|
|
240
|
+
var action2 = function(target, key, descriptor) {
|
|
241
|
+
const isField = typeof key === "object" ? key.kind === "field" : !descriptor;
|
|
242
|
+
return (isField ? actionField : actionMethod)(target, key, descriptor);
|
|
243
|
+
};
|
|
244
|
+
var computed2 = annotation(mobx.computed, "getter");
|
|
245
|
+
function getAnnotations(instance) {
|
|
246
|
+
const chain = [];
|
|
247
|
+
for (let proto = Object.getPrototypeOf(instance); proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) chain.unshift(proto);
|
|
248
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
249
|
+
for (const proto of chain) {
|
|
250
|
+
const constructor = proto.constructor;
|
|
251
|
+
const symbol = Symbol.metadata;
|
|
252
|
+
const metadata = symbol && Object.prototype.hasOwnProperty.call(constructor, symbol) ? constructor[symbol] : void 0;
|
|
253
|
+
const modern = metadata && Object.prototype.hasOwnProperty.call(metadata, ANNOTATIONS) ? metadata[ANNOTATIONS] : void 0;
|
|
254
|
+
for (const own of [legacy.get(proto), modern]) {
|
|
255
|
+
if (!own) continue;
|
|
256
|
+
for (const key of Reflect.ownKeys(own)) {
|
|
257
|
+
if (key in result && result[key] !== own[key]) throw new Error(`[mobx-mantle] Cannot change the inherited annotation of ${String(key)}.`);
|
|
258
|
+
result[key] = own[key];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const key of Reflect.ownKeys(result)) {
|
|
263
|
+
const own = Object.getOwnPropertyDescriptor(instance, key);
|
|
264
|
+
if (own && "value" in own && isServiceValue(instance, own.value)) throw new Error(`[mobx-mantle] Leave service field ${String(key)} undecorated; its resolver owns observability.`);
|
|
265
|
+
}
|
|
266
|
+
return Reflect.ownKeys(result).length ? result : void 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/behavior.ts
|
|
270
|
+
import { makeObservable, observable as observable3, computed as computed3, reaction, autorun, intercept, runInAction as runInAction2 } from "mobx";
|
|
271
|
+
|
|
272
|
+
// src/internals.ts
|
|
273
|
+
import { _getGlobalState, runInAction } from "mobx";
|
|
274
|
+
function smartBind(fn, context) {
|
|
275
|
+
return function(...args) {
|
|
276
|
+
const globalState = _getGlobalState();
|
|
277
|
+
const isInTrackingContext = globalState.trackingDerivation !== null;
|
|
278
|
+
if (isInTrackingContext) {
|
|
279
|
+
return fn.apply(context, args);
|
|
280
|
+
}
|
|
281
|
+
return runInAction(() => fn.apply(context, args));
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (process.env.NODE_ENV !== "production") {
|
|
285
|
+
const globalState = _getGlobalState();
|
|
286
|
+
if (!globalState || !("trackingDerivation" in globalState)) {
|
|
287
|
+
console.warn(
|
|
288
|
+
"[mobx-mantle] MobX internal state no longer exposes trackingDerivation. smartBind cannot detect tracking contexts and props reads cannot skip self-notification, so method calls may not batch correctly and prop changes may double-render. Check mobx version compatibility."
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function toWatchExpression(source, fireImmediately, ownerName) {
|
|
293
|
+
if (typeof source === "function") {
|
|
294
|
+
return source;
|
|
295
|
+
}
|
|
296
|
+
if (process.env.NODE_ENV !== "production" && !fireImmediately) {
|
|
297
|
+
console.warn(
|
|
298
|
+
`[mobx-mantle] ${ownerName}.watch() received a plain value, which can never change \u2014 the callback will never fire. Pass a getter (() => expr) to watch a live source, or add { fireImmediately: true } if a single immediate call is intended.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return () => source;
|
|
302
|
+
}
|
|
303
|
+
function registerReactive(host, create) {
|
|
304
|
+
if (host._mounted) {
|
|
305
|
+
return create();
|
|
306
|
+
}
|
|
307
|
+
const spec = { create, stopped: false, dispose: null };
|
|
308
|
+
host._reactiveSpecs.push(spec);
|
|
309
|
+
return () => {
|
|
310
|
+
spec.stopped = true;
|
|
311
|
+
spec.dispose?.();
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function activateSpecs(host) {
|
|
315
|
+
if (host._wasUnmounted) {
|
|
316
|
+
host._wasUnmounted = false;
|
|
317
|
+
for (const spec of host._reactiveSpecs) {
|
|
318
|
+
if (!spec.stopped) {
|
|
319
|
+
spec.dispose = spec.create();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else if (!host._mounted) {
|
|
323
|
+
for (const spec of host._reactiveSpecs) {
|
|
324
|
+
if (!spec.stopped && spec.dispose === null) {
|
|
325
|
+
spec.dispose = spec.create();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
host._mounted = true;
|
|
330
|
+
}
|
|
331
|
+
function collectProtoInfo(instance, stopProto, excludes, cache) {
|
|
332
|
+
const ctor = instance.constructor;
|
|
333
|
+
const useCache = globalConfig.cacheAnnotations !== false;
|
|
334
|
+
if (useCache) {
|
|
335
|
+
const cached = cache.get(ctor);
|
|
336
|
+
if (cached) return cached;
|
|
337
|
+
}
|
|
338
|
+
const info = { computedKeys: [], methodKeys: [] };
|
|
339
|
+
const seen = /* @__PURE__ */ new Set();
|
|
340
|
+
let proto = Object.getPrototypeOf(instance);
|
|
341
|
+
while (proto && proto !== stopProto && proto !== Object.prototype) {
|
|
342
|
+
const descriptors = Object.getOwnPropertyDescriptors(proto);
|
|
343
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
344
|
+
if (excludes.has(key) || seen.has(key)) continue;
|
|
345
|
+
seen.add(key);
|
|
346
|
+
if (descriptor.get) {
|
|
347
|
+
info.computedKeys.push(key);
|
|
348
|
+
} else if (typeof descriptor.value === "function") {
|
|
349
|
+
info.methodKeys.push(key);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
proto = Object.getPrototypeOf(proto);
|
|
353
|
+
}
|
|
354
|
+
if (useCache) cache.set(ctor, info);
|
|
355
|
+
return info;
|
|
356
|
+
}
|
|
357
|
+
|
|
27
358
|
// src/behavior.ts
|
|
28
|
-
var BEHAVIOR_MARKER = /* @__PURE__ */ Symbol("behavior");
|
|
359
|
+
var BEHAVIOR_MARKER = /* @__PURE__ */ Symbol.for("mobx-mantle.behavior");
|
|
29
360
|
var BEHAVIOR_EXCLUDES = /* @__PURE__ */ new Set([
|
|
30
361
|
"onCreate",
|
|
31
362
|
"onLayoutMount",
|
|
32
363
|
"onMount",
|
|
33
364
|
"onUnmount",
|
|
365
|
+
"addCleanup",
|
|
366
|
+
"_addCleanup",
|
|
34
367
|
"watch",
|
|
368
|
+
"effect",
|
|
369
|
+
"sync",
|
|
35
370
|
"constructor",
|
|
371
|
+
"inject",
|
|
36
372
|
"_watchDisposers",
|
|
37
|
-
"_disposeWatchers"
|
|
373
|
+
"_disposeWatchers",
|
|
374
|
+
"_reactiveSpecs",
|
|
375
|
+
"_behaviors",
|
|
376
|
+
"_pendingSyncs",
|
|
377
|
+
"_syncWriting",
|
|
378
|
+
"_mounted",
|
|
379
|
+
"_wasUnmounted"
|
|
38
380
|
]);
|
|
381
|
+
var SyncMarker = class {
|
|
382
|
+
constructor(source) {
|
|
383
|
+
this.source = source;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
39
386
|
function isRefLike(value) {
|
|
40
387
|
if (value === null || typeof value !== "object") return false;
|
|
41
388
|
if (Array.isArray(value)) return false;
|
|
@@ -45,16 +392,72 @@ var Behavior = class {
|
|
|
45
392
|
constructor() {
|
|
46
393
|
/** @internal */
|
|
47
394
|
this._watchDisposers = [];
|
|
395
|
+
/** @internal — pre-mount watch/effect registrations, materialized at mount */
|
|
396
|
+
this._reactiveSpecs = [];
|
|
397
|
+
/** @internal — child behaviors declared as fields, collected at construction */
|
|
398
|
+
this._behaviors = [];
|
|
399
|
+
/** @internal — true after the parent Component first mounts */
|
|
400
|
+
this._mounted = false;
|
|
401
|
+
/** @internal — set when effects clean up so StrictMode replay can resurrect pre-mount watchers */
|
|
402
|
+
this._wasUnmounted = false;
|
|
403
|
+
/** @internal — sync() markers awaiting the post-onCreate field scan; null once construction completes */
|
|
404
|
+
this._pendingSyncs = [];
|
|
405
|
+
/** @internal — true while the hidden sync effect writes a mirrored field (dev write-guard) */
|
|
406
|
+
this._syncWriting = false;
|
|
407
|
+
captureServiceScope(this);
|
|
408
|
+
}
|
|
409
|
+
inject(token) {
|
|
410
|
+
return resolveService(this, token);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Register a cleanup function to run automatically on unmount.
|
|
414
|
+
* Returns a function that can be called for early cleanup.
|
|
415
|
+
*
|
|
416
|
+
* Cleanups are one-shot: they are not re-created during React StrictMode's
|
|
417
|
+
* same-instance effect replay. Call this from onMount (which is replayed),
|
|
418
|
+
* or use effect() for a replay-safe setup/teardown pair.
|
|
419
|
+
*/
|
|
420
|
+
addCleanup(cleanup) {
|
|
421
|
+
if (process.env.NODE_ENV !== "production" && !this._mounted) {
|
|
422
|
+
console.warn(
|
|
423
|
+
`[mobx-mantle] ${this.constructor.name}.addCleanup() called before mount. Cleanups are one-shot: they run when mount effects clean up and are not re-created during React StrictMode's same-instance effect replay. Acquire resources in onMount(), or use effect() for a replay-safe setup/teardown pair.`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return this._addCleanup(cleanup);
|
|
427
|
+
}
|
|
428
|
+
/** @internal — addCleanup without the pre-mount dev warning (used by watch/effect) */
|
|
429
|
+
_addCleanup(cleanup) {
|
|
430
|
+
let active = true;
|
|
431
|
+
const dispose = () => {
|
|
432
|
+
if (!active) return;
|
|
433
|
+
active = false;
|
|
434
|
+
try {
|
|
435
|
+
runInAction2(cleanup);
|
|
436
|
+
} finally {
|
|
437
|
+
const idx = this._watchDisposers.indexOf(dispose);
|
|
438
|
+
if (idx !== -1) this._watchDisposers.splice(idx, 1);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
this._watchDisposers.push(dispose);
|
|
442
|
+
return dispose;
|
|
48
443
|
}
|
|
49
444
|
/**
|
|
50
445
|
* Watch a reactive expression and run a callback when it changes.
|
|
51
|
-
* Automatically disposed on unmount
|
|
52
|
-
*
|
|
53
|
-
*
|
|
446
|
+
* Automatically disposed on unmount and re-created during React StrictMode's
|
|
447
|
+
* same-instance effect replay.
|
|
448
|
+
*
|
|
449
|
+
* Safe to call from onCreate: registration is recorded and comes alive
|
|
450
|
+
* when the parent commits (just before first paint), so the watcher
|
|
451
|
+
* tracks observable fields correctly and renders that React discards
|
|
452
|
+
* never leak reactions.
|
|
453
|
+
*
|
|
454
|
+
* @param source - Reactive expression (getter) to watch, or a MaybeGetter
|
|
455
|
+
* argument passed through as-is. To watch a value that is itself a
|
|
456
|
+
* function, wrap it: `watch(() => this.callback, …)`
|
|
54
457
|
* @param callback - Called when the expression result changes
|
|
55
458
|
* @param options - Optional configuration (delay, fireImmediately)
|
|
56
459
|
* @returns Dispose function for early teardown
|
|
57
|
-
*
|
|
460
|
+
*
|
|
58
461
|
* @example
|
|
59
462
|
* ```tsx
|
|
60
463
|
* onCreate(url: string) {
|
|
@@ -63,36 +466,119 @@ var Behavior = class {
|
|
|
63
466
|
* }
|
|
64
467
|
* ```
|
|
65
468
|
*/
|
|
66
|
-
watch(
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
469
|
+
watch(source, callback, options) {
|
|
470
|
+
const expr = toWatchExpression(source, options?.fireImmediately, this.constructor.name);
|
|
471
|
+
return registerReactive(this, () => {
|
|
472
|
+
const dispose = reaction(
|
|
473
|
+
expr,
|
|
474
|
+
(value, prevValue) => {
|
|
475
|
+
try {
|
|
476
|
+
callback(value, prevValue);
|
|
477
|
+
} catch (e) {
|
|
478
|
+
reportError(e, { phase: "watch", name: this.constructor.name, isBehavior: true });
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
delay: options?.delay,
|
|
483
|
+
fireImmediately: options?.fireImmediately
|
|
74
484
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
485
|
+
);
|
|
486
|
+
return this._addCleanup(dispose);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Run a side effect that auto-tracks reactive dependencies.
|
|
491
|
+
* Re-runs whenever any accessed observable changes.
|
|
492
|
+
* Automatically disposed on unmount.
|
|
493
|
+
*
|
|
494
|
+
* Best for simple synchronization (DOM updates, logging). For complex
|
|
495
|
+
* side effects with explicit triggers, prefer `watch()`.
|
|
496
|
+
*
|
|
497
|
+
* @param fn - Effect function. May return a cleanup function.
|
|
498
|
+
* @param options - Optional configuration (delay)
|
|
499
|
+
* @returns Dispose function for early teardown
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* ```tsx
|
|
503
|
+
* onCreate() {
|
|
504
|
+
* this.effect(() => {
|
|
505
|
+
* console.log('Current value:', this.value);
|
|
506
|
+
* });
|
|
507
|
+
* }
|
|
508
|
+
* ```
|
|
509
|
+
*/
|
|
510
|
+
effect(fn, options) {
|
|
511
|
+
return registerReactive(this, () => {
|
|
512
|
+
let cleanup;
|
|
513
|
+
const dispose = autorun(
|
|
514
|
+
() => {
|
|
515
|
+
runInAction2(() => cleanup?.());
|
|
516
|
+
cleanup = void 0;
|
|
517
|
+
try {
|
|
518
|
+
const result = fn();
|
|
519
|
+
if (typeof result === "function") {
|
|
520
|
+
cleanup = result;
|
|
521
|
+
}
|
|
522
|
+
} catch (e) {
|
|
523
|
+
reportError(e, { phase: "effect", name: this.constructor.name, isBehavior: true });
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
{ delay: options?.delay }
|
|
527
|
+
);
|
|
528
|
+
return this._addCleanup(() => {
|
|
529
|
+
runInAction2(() => cleanup?.());
|
|
530
|
+
dispose();
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Normalize a MaybeGetter argument into an ordinary field that stays
|
|
536
|
+
* current. Call once in onCreate (or a field initializer) and assign the
|
|
537
|
+
* result to a field; everywhere else, read the field like normal state:
|
|
538
|
+
*
|
|
539
|
+
* ```tsx
|
|
540
|
+
* url!: string;
|
|
541
|
+
*
|
|
542
|
+
* onCreate(url: MaybeGetter<string>) {
|
|
543
|
+
* this.url = this.sync(url); // the one convention line
|
|
544
|
+
* this.watch(() => this.url, (u) => this.load(u));
|
|
545
|
+
* }
|
|
546
|
+
* refresh() { this.load(this.url); } // plain read, always current
|
|
547
|
+
* ```
|
|
548
|
+
*
|
|
549
|
+
* - Plain value → the field just holds it (no effect registered).
|
|
550
|
+
* - Getter → a hidden effect keeps the field updated whenever the getter's
|
|
551
|
+
* observable sources change (alive from mount, like watch/effect).
|
|
552
|
+
*
|
|
553
|
+
* The sync is one-way (argument → field). Don't write to the field from
|
|
554
|
+
* outside: the next source change overwrites it (dev mode warns).
|
|
555
|
+
*/
|
|
556
|
+
sync(source) {
|
|
557
|
+
if (typeof source !== "function") {
|
|
558
|
+
return source;
|
|
559
|
+
}
|
|
560
|
+
if (this._pendingSyncs === null) {
|
|
561
|
+
if (process.env.NODE_ENV !== "production") {
|
|
562
|
+
console.warn(
|
|
563
|
+
`[mobx-mantle] ${this.constructor.name}.sync() called after construction. sync() only works in field initializers or onCreate; returning a one-time snapshot instead. Use watch()/effect() for post-mount reactions.`
|
|
564
|
+
);
|
|
79
565
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (idx !== -1) this._watchDisposers.splice(idx, 1);
|
|
86
|
-
};
|
|
566
|
+
return toValue(source);
|
|
567
|
+
}
|
|
568
|
+
const marker = new SyncMarker(source);
|
|
569
|
+
this._pendingSyncs.push(marker);
|
|
570
|
+
return marker;
|
|
87
571
|
}
|
|
88
572
|
/** @internal */
|
|
89
573
|
_disposeWatchers() {
|
|
90
|
-
|
|
574
|
+
const disposers = this._watchDisposers.slice();
|
|
575
|
+
this._watchDisposers.length = 0;
|
|
576
|
+
for (const dispose of disposers) {
|
|
91
577
|
dispose();
|
|
92
578
|
}
|
|
93
|
-
this._watchDisposers.length = 0;
|
|
94
579
|
}
|
|
95
580
|
};
|
|
581
|
+
var behaviorProtoInfo = /* @__PURE__ */ new WeakMap();
|
|
96
582
|
function makeBehaviorObservable(instance) {
|
|
97
583
|
const annotations = {};
|
|
98
584
|
const ownKeys = /* @__PURE__ */ new Set([
|
|
@@ -103,43 +589,118 @@ function makeBehaviorObservable(instance) {
|
|
|
103
589
|
if (BEHAVIOR_EXCLUDES.has(key)) continue;
|
|
104
590
|
if (key in annotations) continue;
|
|
105
591
|
const value = instance[key];
|
|
106
|
-
if (typeof value === "function") continue;
|
|
592
|
+
if (typeof value === "function" || isServiceValue(instance, value)) continue;
|
|
593
|
+
if (isBehavior(value)) {
|
|
594
|
+
annotations[key] = observable3.ref;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
107
597
|
if (isRefLike(value)) {
|
|
108
|
-
annotations[key] =
|
|
598
|
+
annotations[key] = observable3.ref;
|
|
109
599
|
} else {
|
|
110
|
-
annotations[key] =
|
|
600
|
+
annotations[key] = observable3;
|
|
111
601
|
}
|
|
112
602
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (BEHAVIOR_EXCLUDES.has(key)) continue;
|
|
118
|
-
if (key in annotations) continue;
|
|
119
|
-
if (descriptor.get) {
|
|
120
|
-
annotations[key] = computed;
|
|
121
|
-
} else if (typeof descriptor.value === "function") {
|
|
122
|
-
annotations[key] = action.bound;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
proto = Object.getPrototypeOf(proto);
|
|
603
|
+
const protoInfo = collectProtoInfo(instance, Behavior.prototype, BEHAVIOR_EXCLUDES, behaviorProtoInfo);
|
|
604
|
+
for (const key of protoInfo.computedKeys) {
|
|
605
|
+
if (key in annotations) continue;
|
|
606
|
+
annotations[key] = computed3;
|
|
126
607
|
}
|
|
127
608
|
makeObservable(instance, annotations);
|
|
609
|
+
for (const key of protoInfo.methodKeys) {
|
|
610
|
+
if (key in annotations) continue;
|
|
611
|
+
const method = instance[key];
|
|
612
|
+
if (typeof method === "function") {
|
|
613
|
+
instance[key] = smartBind(method, instance);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
128
616
|
}
|
|
129
617
|
function createBehavior(Def, options) {
|
|
130
618
|
var _a, _b, _c;
|
|
131
619
|
const BehaviorClass = (_c = class extends (_b = Def, _a = BEHAVIOR_MARKER, _b) {
|
|
132
620
|
constructor(...args) {
|
|
621
|
+
applyMobxActionPolicy();
|
|
133
622
|
super(...args);
|
|
134
623
|
if (typeof this.onCreate === "function") {
|
|
135
|
-
this.onCreate(...args);
|
|
624
|
+
withOwnerServiceScope(this, () => runInAction2(() => this.onCreate(...args)));
|
|
625
|
+
}
|
|
626
|
+
const children = [];
|
|
627
|
+
for (const key of Object.keys(this)) {
|
|
628
|
+
if (key.startsWith("_")) continue;
|
|
629
|
+
const value = this[key];
|
|
630
|
+
if (isBehavior(value) && !isServiceValue(this, value)) {
|
|
631
|
+
children.push({ instance: value });
|
|
632
|
+
}
|
|
136
633
|
}
|
|
634
|
+
this._behaviors = children;
|
|
635
|
+
const syncedFields = [];
|
|
636
|
+
const pending = this._pendingSyncs ?? null;
|
|
637
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
638
|
+
for (const key of Object.keys(this)) {
|
|
639
|
+
const value = this[key];
|
|
640
|
+
if (value instanceof SyncMarker) {
|
|
641
|
+
syncedFields.push({ key, source: value.source });
|
|
642
|
+
this[key] = value.source();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (process.env.NODE_ENV !== "production" && syncedFields.length !== pending.length) {
|
|
646
|
+
console.warn(
|
|
647
|
+
`[mobx-mantle] ${Def.name}: ${pending.length - syncedFields.length} sync() result(s) were not assigned to a field. Assign sync() directly to a field (this.url = this.sync(url)) so Mantle can keep it updated.`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
this._pendingSyncs = null;
|
|
137
652
|
const autoObservable = options?.autoObservable ?? globalConfig.autoObservable;
|
|
138
|
-
|
|
653
|
+
const annotations = getAnnotations(this);
|
|
654
|
+
if (annotations) {
|
|
655
|
+
makeObservable(this, annotations);
|
|
656
|
+
const info = collectProtoInfo(this, Behavior.prototype, BEHAVIOR_EXCLUDES, behaviorProtoInfo);
|
|
657
|
+
for (const key of info.methodKeys) if (!(key in annotations)) this[key] = smartBind(this[key], this);
|
|
658
|
+
} else if (autoObservable) {
|
|
139
659
|
makeBehaviorObservable(this);
|
|
140
660
|
} else {
|
|
141
661
|
makeObservable(this);
|
|
142
662
|
}
|
|
663
|
+
if (syncedFields.length > 0) {
|
|
664
|
+
const self = this;
|
|
665
|
+
const specs = syncedFields.map(({ key, source }) => ({
|
|
666
|
+
create: () => {
|
|
667
|
+
const dispose = autorun(() => {
|
|
668
|
+
try {
|
|
669
|
+
const next = source();
|
|
670
|
+
self._syncWriting = true;
|
|
671
|
+
try {
|
|
672
|
+
runInAction2(() => {
|
|
673
|
+
self[key] = next;
|
|
674
|
+
});
|
|
675
|
+
} finally {
|
|
676
|
+
self._syncWriting = false;
|
|
677
|
+
}
|
|
678
|
+
} catch (e) {
|
|
679
|
+
reportError(e, { phase: "sync", name: self.constructor.name, isBehavior: true });
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
return self._addCleanup(dispose);
|
|
683
|
+
},
|
|
684
|
+
stopped: false,
|
|
685
|
+
dispose: null
|
|
686
|
+
}));
|
|
687
|
+
self._reactiveSpecs.unshift(...specs);
|
|
688
|
+
if (process.env.NODE_ENV !== "production") {
|
|
689
|
+
for (const { key } of syncedFields) {
|
|
690
|
+
try {
|
|
691
|
+
intercept(self, key, (change) => {
|
|
692
|
+
if (!self._syncWriting) {
|
|
693
|
+
console.warn(
|
|
694
|
+
`[mobx-mantle] ${Def.name}.${key} is a one-way synced field (this.sync()). Writing to it from outside will be overwritten by the next source change. Derive with a computed getter, or own the state in a plain field instead.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return change;
|
|
698
|
+
});
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
143
704
|
}
|
|
144
705
|
}, _c[_a] = true, _c);
|
|
145
706
|
Object.defineProperty(BehaviorClass, "name", { value: Def.name });
|
|
@@ -153,33 +714,55 @@ function isBehavior(value) {
|
|
|
153
714
|
if (value === null || typeof value !== "object") return false;
|
|
154
715
|
return value.constructor?.[BEHAVIOR_MARKER] === true;
|
|
155
716
|
}
|
|
156
|
-
function
|
|
717
|
+
function childBehaviors(inst) {
|
|
718
|
+
return Array.isArray(inst._behaviors) ? inst._behaviors : [];
|
|
719
|
+
}
|
|
720
|
+
function layoutMountBehavior(behavior, visited = /* @__PURE__ */ new WeakSet()) {
|
|
157
721
|
const inst = behavior.instance;
|
|
722
|
+
if (visited.has(inst)) return;
|
|
723
|
+
visited.add(inst);
|
|
724
|
+
for (const child of childBehaviors(inst)) {
|
|
725
|
+
layoutMountBehavior(child, visited);
|
|
726
|
+
}
|
|
727
|
+
if (Array.isArray(inst._reactiveSpecs)) {
|
|
728
|
+
activateSpecs(inst);
|
|
729
|
+
} else {
|
|
730
|
+
inst._mounted = true;
|
|
731
|
+
}
|
|
158
732
|
if ("onLayoutMount" in inst && typeof inst.onLayoutMount === "function") {
|
|
159
733
|
try {
|
|
160
|
-
|
|
734
|
+
const result = runInAction2(() => inst.onLayoutMount());
|
|
735
|
+
behavior.layoutCleanup = typeof result === "function" ? result : void 0;
|
|
161
736
|
} catch (e) {
|
|
162
737
|
reportError(e, { phase: "onLayoutMount", name: inst.constructor.name, isBehavior: true });
|
|
163
738
|
}
|
|
164
739
|
}
|
|
165
740
|
}
|
|
166
|
-
function mountBehavior(behavior) {
|
|
741
|
+
function mountBehavior(behavior, visited = /* @__PURE__ */ new WeakSet()) {
|
|
167
742
|
const inst = behavior.instance;
|
|
743
|
+
if (visited.has(inst)) return;
|
|
744
|
+
visited.add(inst);
|
|
745
|
+
for (const child of childBehaviors(inst)) {
|
|
746
|
+
mountBehavior(child, visited);
|
|
747
|
+
}
|
|
168
748
|
if ("onMount" in inst && typeof inst.onMount === "function") {
|
|
169
749
|
try {
|
|
170
|
-
|
|
750
|
+
const result = runInAction2(() => inst.onMount());
|
|
751
|
+
behavior.cleanup = typeof result === "function" ? result : void 0;
|
|
171
752
|
} catch (e) {
|
|
172
753
|
reportError(e, { phase: "onMount", name: inst.constructor.name, isBehavior: true });
|
|
173
754
|
}
|
|
174
755
|
}
|
|
175
756
|
}
|
|
176
|
-
function unmountBehavior(behavior) {
|
|
177
|
-
behavior.layoutCleanup?.();
|
|
178
|
-
behavior.cleanup?.();
|
|
757
|
+
function unmountBehavior(behavior, visited = /* @__PURE__ */ new WeakSet()) {
|
|
179
758
|
const inst = behavior.instance;
|
|
759
|
+
if (visited.has(inst)) return;
|
|
760
|
+
visited.add(inst);
|
|
761
|
+
runInAction2(() => behavior.layoutCleanup?.());
|
|
762
|
+
runInAction2(() => behavior.cleanup?.());
|
|
180
763
|
if ("onUnmount" in inst && typeof inst.onUnmount === "function") {
|
|
181
764
|
try {
|
|
182
|
-
inst.onUnmount();
|
|
765
|
+
runInAction2(() => inst.onUnmount());
|
|
183
766
|
} catch (e) {
|
|
184
767
|
reportError(e, { phase: "onUnmount", name: inst.constructor.name, isBehavior: true });
|
|
185
768
|
}
|
|
@@ -187,44 +770,75 @@ function unmountBehavior(behavior) {
|
|
|
187
770
|
if (typeof inst._disposeWatchers === "function") {
|
|
188
771
|
inst._disposeWatchers();
|
|
189
772
|
}
|
|
773
|
+
inst._wasUnmounted = true;
|
|
774
|
+
const children = childBehaviors(inst);
|
|
775
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
776
|
+
unmountBehavior(children[i], visited);
|
|
777
|
+
}
|
|
190
778
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
779
|
+
function warnUncollectedBehaviors(host, hostName, entries, visited = /* @__PURE__ */ new WeakSet()) {
|
|
780
|
+
if (visited.has(host)) return;
|
|
781
|
+
visited.add(host);
|
|
782
|
+
const collected = new Set(entries.map((e) => e.instance));
|
|
783
|
+
for (const key of Object.keys(host)) {
|
|
784
|
+
if (key.startsWith("_")) continue;
|
|
785
|
+
const value = host[key];
|
|
786
|
+
if (isBehavior(value) && !isServiceValue(host, value) && !collected.has(value)) {
|
|
787
|
+
console.warn(
|
|
788
|
+
`[mobx-mantle] ${hostName}.${key} holds a behavior that was assigned after construction, so its lifecycle (onMount, watchers, cleanup) will never run. Create behaviors as class fields so they are collected when the host is constructed.`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const entry of entries) {
|
|
793
|
+
warnUncollectedBehaviors(
|
|
794
|
+
entry.instance,
|
|
795
|
+
entry.instance.constructor.name,
|
|
796
|
+
childBehaviors(entry.instance),
|
|
797
|
+
visited
|
|
798
|
+
);
|
|
799
|
+
}
|
|
199
800
|
}
|
|
200
|
-
|
|
201
|
-
|
|
801
|
+
|
|
802
|
+
// src/mantle.tsx
|
|
803
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
804
|
+
var componentRefs = /* @__PURE__ */ new WeakSet();
|
|
805
|
+
var ComponentParentContext = React4.createContext(void 0);
|
|
806
|
+
var componentParents = /* @__PURE__ */ new WeakMap();
|
|
807
|
+
var ownerScopes = /* @__PURE__ */ new WeakMap();
|
|
808
|
+
function ownerScope(owner) {
|
|
809
|
+
let scope = ownerScopes.get(owner);
|
|
810
|
+
if (!scope) {
|
|
811
|
+
scope = { handle: { current: owner }, resolver: getServiceResolver(owner), substitutions: void 0 };
|
|
812
|
+
ownerScopes.set(owner, scope);
|
|
813
|
+
}
|
|
814
|
+
return scope;
|
|
202
815
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
206
|
-
observable3.shallow = function(_value, context) {
|
|
207
|
-
setAnnotation(context, mobx.observable.shallow);
|
|
208
|
-
};
|
|
209
|
-
observable3.struct = function(_value, context) {
|
|
210
|
-
setAnnotation(context, mobx.observable.struct);
|
|
211
|
-
};
|
|
212
|
-
observable3.deep = function(_value, context) {
|
|
213
|
-
setAnnotation(context, mobx.observable.deep);
|
|
214
|
-
};
|
|
215
|
-
function action3(_value, context) {
|
|
216
|
-
setAnnotation(context, mobx.action.bound);
|
|
816
|
+
function withRef(props, ref) {
|
|
817
|
+
return ref == null ? props : { ...props, ref };
|
|
217
818
|
}
|
|
218
|
-
|
|
219
|
-
|
|
819
|
+
var activeComponentConstruction;
|
|
820
|
+
function constructComponent(ComponentClass, props, parentHandle) {
|
|
821
|
+
const previousScope = activeComponentConstruction;
|
|
822
|
+
const scope = {
|
|
823
|
+
expectedClass: ComponentClass,
|
|
824
|
+
parentHandle,
|
|
825
|
+
consumed: false
|
|
826
|
+
};
|
|
827
|
+
activeComponentConstruction = scope;
|
|
828
|
+
try {
|
|
829
|
+
return new ComponentClass(props);
|
|
830
|
+
} finally {
|
|
831
|
+
activeComponentConstruction = previousScope;
|
|
832
|
+
}
|
|
220
833
|
}
|
|
221
|
-
function
|
|
222
|
-
const
|
|
223
|
-
|
|
834
|
+
function captureConstructingComponentParent(instance) {
|
|
835
|
+
const scope = activeComponentConstruction;
|
|
836
|
+
if (!scope || scope.consumed || instance.constructor !== scope.expectedClass) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
scope.consumed = true;
|
|
840
|
+
componentParents.set(instance, scope.parentHandle);
|
|
224
841
|
}
|
|
225
|
-
|
|
226
|
-
// src/mantle.tsx
|
|
227
|
-
var viewRefs = /* @__PURE__ */ new WeakSet();
|
|
228
842
|
function shallowEqual(a, b) {
|
|
229
843
|
if (a === b) return true;
|
|
230
844
|
if (!a || !b) return false;
|
|
@@ -236,37 +850,168 @@ function shallowEqual(a, b) {
|
|
|
236
850
|
}
|
|
237
851
|
return true;
|
|
238
852
|
}
|
|
239
|
-
var
|
|
240
|
-
constructor() {
|
|
853
|
+
var PropsBox = class {
|
|
854
|
+
constructor(value, name) {
|
|
855
|
+
/**
|
|
856
|
+
* @internal The owning component's render reaction. Reads coming from it
|
|
857
|
+
* are not tracked: React already re-renders the component on prop change
|
|
858
|
+
* (via memo), so letting the render reaction observe the props atom would
|
|
859
|
+
* only schedule a redundant second render after every prop change.
|
|
860
|
+
* Computeds, watchers, and other components track normally.
|
|
861
|
+
*/
|
|
862
|
+
this._renderReaction = null;
|
|
863
|
+
this._value = value;
|
|
864
|
+
this._atom = createAtom(name);
|
|
865
|
+
}
|
|
866
|
+
get() {
|
|
867
|
+
const own = this._renderReaction?.current;
|
|
868
|
+
if (!own || _getGlobalState2().trackingDerivation !== own) {
|
|
869
|
+
this._atom.reportObserved();
|
|
870
|
+
}
|
|
871
|
+
return this._value;
|
|
872
|
+
}
|
|
873
|
+
setSilently(value) {
|
|
874
|
+
this._value = value;
|
|
875
|
+
}
|
|
876
|
+
set(value) {
|
|
877
|
+
this._value = value;
|
|
878
|
+
this._atom.reportChanged();
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
var Component = class {
|
|
882
|
+
constructor(props) {
|
|
241
883
|
/** @internal */
|
|
242
884
|
this._behaviors = [];
|
|
243
885
|
/** @internal */
|
|
244
886
|
this._watchDisposers = [];
|
|
887
|
+
/** @internal — pre-mount watch/effect registrations, materialized at mount */
|
|
888
|
+
this._reactiveSpecs = [];
|
|
889
|
+
/** @internal — true after the first (layout) mount */
|
|
890
|
+
this._mounted = false;
|
|
891
|
+
/** @internal — set when effects clean up so StrictMode replay can resurrect watchers */
|
|
892
|
+
this._wasUnmounted = false;
|
|
893
|
+
this._propsBox = new PropsBox(props, `${this.constructor.name}.props`);
|
|
894
|
+
captureConstructingComponentParent(this);
|
|
895
|
+
captureServiceScope(this);
|
|
245
896
|
}
|
|
246
897
|
get props() {
|
|
247
898
|
return this._propsBox.get();
|
|
248
899
|
}
|
|
249
|
-
/**
|
|
900
|
+
/** Resolve through the scope captured during construction; preserve service identity. */
|
|
901
|
+
inject(token) {
|
|
902
|
+
return resolveService(this, token);
|
|
903
|
+
}
|
|
904
|
+
/** Return the immediate ancestor in the live Mantle view tree. */
|
|
905
|
+
getParent() {
|
|
906
|
+
return componentParents.get(this)?.current;
|
|
907
|
+
}
|
|
908
|
+
/** Find the nearest parent with the supplied logical model identity. */
|
|
909
|
+
findParent(type) {
|
|
910
|
+
for (const parent of this.getParents()) {
|
|
911
|
+
if (isModel(parent, type)) return parent;
|
|
912
|
+
}
|
|
913
|
+
return void 0;
|
|
914
|
+
}
|
|
915
|
+
/** Return all Mantle parents, ordered from the immediate parent to the root. */
|
|
916
|
+
getParents() {
|
|
917
|
+
const parents = [];
|
|
918
|
+
let current = this.getParent();
|
|
919
|
+
while (current) {
|
|
920
|
+
parents.push(current);
|
|
921
|
+
current = current.getParent();
|
|
922
|
+
}
|
|
923
|
+
return parents;
|
|
924
|
+
}
|
|
925
|
+
/** Return the highest Mantle ancestor, or this instance when already root. */
|
|
926
|
+
getRoot() {
|
|
927
|
+
const parents = this.getParents();
|
|
928
|
+
return parents[parents.length - 1] ?? this;
|
|
929
|
+
}
|
|
930
|
+
/** @internal — called by createComponent to silently update props during render */
|
|
250
931
|
_syncProps(value) {
|
|
251
|
-
this._propsBox.
|
|
932
|
+
this._propsBox.setSilently(value);
|
|
252
933
|
}
|
|
253
934
|
ref() {
|
|
254
935
|
const r = { current: null };
|
|
255
|
-
|
|
936
|
+
componentRefs.add(r);
|
|
256
937
|
return r;
|
|
257
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* A tracked view of this instance, for handing to code you don't control
|
|
941
|
+
* (a table's row renderer, an error boundary's fallback, a map popup).
|
|
942
|
+
* Created once, so its identity is stable. Re-renders on its own when the
|
|
943
|
+
* state it reads changes. Forwards refs. Carries this model's context, so
|
|
944
|
+
* `findParent()` and services resolve inside it even when a library renders
|
|
945
|
+
* it in its own React root.
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* ```tsx
|
|
949
|
+
* row = this.view((props: RowProps) => (
|
|
950
|
+
* <tr {...props} className={props.id === this.activeId ? 'active' : ''} />
|
|
951
|
+
* ));
|
|
952
|
+
* ```
|
|
953
|
+
*/
|
|
954
|
+
view(render) {
|
|
955
|
+
const owner = this;
|
|
956
|
+
const name = `${owner.constructor.name}.${render.name || "view"}`;
|
|
957
|
+
const View = reactForwardRef((props, ref) => {
|
|
958
|
+
const scope = ownerScope(owner);
|
|
959
|
+
const rendered = useMantleObserver(() => render(withRef(props, ref)), name);
|
|
960
|
+
return /* @__PURE__ */ jsx3(ComponentParentContext.Provider, { value: scope.handle, children: /* @__PURE__ */ jsx3(ServiceContext.Provider, { value: scope.resolver, children: /* @__PURE__ */ jsx3(ModelContext.Provider, { value: scope.substitutions, children: rendered }) }) });
|
|
961
|
+
});
|
|
962
|
+
View.displayName = name;
|
|
963
|
+
const Memoized = memo2(View);
|
|
964
|
+
trackedComponents.add(Memoized);
|
|
965
|
+
return Memoized;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Register a cleanup function to run automatically on unmount.
|
|
969
|
+
* Returns a function that can be called for early cleanup.
|
|
970
|
+
*
|
|
971
|
+
* Cleanups are one-shot: they are not re-created during React StrictMode's
|
|
972
|
+
* same-instance effect replay. Call this from onMount (which is replayed),
|
|
973
|
+
* or use effect() for a replay-safe setup/teardown pair.
|
|
974
|
+
*/
|
|
975
|
+
addCleanup(cleanup) {
|
|
976
|
+
if (process.env.NODE_ENV !== "production" && !this._mounted) {
|
|
977
|
+
console.warn(
|
|
978
|
+
`[mobx-mantle] ${this.constructor.name}.addCleanup() called before mount. Cleanups are one-shot: they run when mount effects clean up and are not re-created during React StrictMode's same-instance effect replay. Acquire resources in onMount(), or use effect() for a replay-safe setup/teardown pair.`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
return this._addCleanup(cleanup);
|
|
982
|
+
}
|
|
983
|
+
/** @internal — addCleanup without the pre-mount dev warning (used by watch/effect) */
|
|
984
|
+
_addCleanup(cleanup) {
|
|
985
|
+
let active = true;
|
|
986
|
+
const dispose = () => {
|
|
987
|
+
if (!active) return;
|
|
988
|
+
active = false;
|
|
989
|
+
try {
|
|
990
|
+
runInAction3(cleanup);
|
|
991
|
+
} finally {
|
|
992
|
+
const idx = this._watchDisposers.indexOf(dispose);
|
|
993
|
+
if (idx !== -1) this._watchDisposers.splice(idx, 1);
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
this._watchDisposers.push(dispose);
|
|
997
|
+
return dispose;
|
|
998
|
+
}
|
|
258
999
|
/**
|
|
259
1000
|
* Watch a reactive expression and run a callback when it changes.
|
|
260
|
-
* Automatically disposed on unmount.
|
|
261
|
-
*
|
|
262
|
-
*
|
|
1001
|
+
* Automatically disposed on unmount. Watchers declared before mount
|
|
1002
|
+
* (field initializers, onCreate) are re-created during React StrictMode's
|
|
1003
|
+
* same-instance effect replay.
|
|
1004
|
+
*
|
|
1005
|
+
* @param source - Reactive expression (getter) to watch, or a MaybeGetter
|
|
1006
|
+
* argument passed through as-is. To watch a value that is itself a
|
|
1007
|
+
* function, wrap it: `watch(() => this.callback, …)`
|
|
263
1008
|
* @param callback - Called when the expression result changes
|
|
264
1009
|
* @param options - Optional configuration (delay, fireImmediately)
|
|
265
1010
|
* @returns Dispose function for early teardown
|
|
266
1011
|
*
|
|
267
1012
|
* @example
|
|
268
1013
|
* ```tsx
|
|
269
|
-
*
|
|
1014
|
+
* onMount() {
|
|
270
1015
|
* this.watch(
|
|
271
1016
|
* () => this.query,
|
|
272
1017
|
* async (query) => {
|
|
@@ -279,41 +1024,98 @@ var View = class {
|
|
|
279
1024
|
* }
|
|
280
1025
|
* ```
|
|
281
1026
|
*/
|
|
282
|
-
watch(
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1027
|
+
watch(source, callback, options) {
|
|
1028
|
+
const expr = toWatchExpression(source, options?.fireImmediately, this.constructor.name);
|
|
1029
|
+
return registerReactive(this, () => {
|
|
1030
|
+
const dispose = reaction2(
|
|
1031
|
+
expr,
|
|
1032
|
+
(value, prevValue) => {
|
|
1033
|
+
try {
|
|
1034
|
+
callback(value, prevValue);
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
reportError(e, { phase: "watch", name: this.constructor.name, isBehavior: false });
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
delay: options?.delay,
|
|
1041
|
+
fireImmediately: options?.fireImmediately
|
|
290
1042
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
1043
|
+
);
|
|
1044
|
+
return this._addCleanup(dispose);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Run a side effect that auto-tracks reactive dependencies.
|
|
1049
|
+
* Re-runs whenever any accessed observable changes.
|
|
1050
|
+
* Automatically disposed on unmount. Effects declared before mount
|
|
1051
|
+
* (field initializers, onCreate) are re-created during React StrictMode's
|
|
1052
|
+
* same-instance effect replay.
|
|
1053
|
+
*
|
|
1054
|
+
* Best for simple synchronization (DOM updates, logging). For complex
|
|
1055
|
+
* side effects with explicit triggers, prefer `watch()`.
|
|
1056
|
+
*
|
|
1057
|
+
* @param fn - Effect function. May return a cleanup function.
|
|
1058
|
+
* @param options - Optional configuration (delay)
|
|
1059
|
+
* @returns Dispose function for early teardown
|
|
1060
|
+
*
|
|
1061
|
+
* @example
|
|
1062
|
+
* ```tsx
|
|
1063
|
+
* onMount() {
|
|
1064
|
+
* this.effect(() => {
|
|
1065
|
+
* document.title = `${this.items.length} items`;
|
|
1066
|
+
* });
|
|
1067
|
+
* }
|
|
1068
|
+
* ```
|
|
1069
|
+
*
|
|
1070
|
+
* @example With cleanup
|
|
1071
|
+
* ```tsx
|
|
1072
|
+
* onMount() {
|
|
1073
|
+
* this.effect(() => {
|
|
1074
|
+
* const handler = () => console.log(this.count);
|
|
1075
|
+
* window.addEventListener('click', handler);
|
|
1076
|
+
* return () => window.removeEventListener('click', handler);
|
|
1077
|
+
* });
|
|
1078
|
+
* }
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
effect(fn, options) {
|
|
1082
|
+
return registerReactive(this, () => {
|
|
1083
|
+
let cleanup;
|
|
1084
|
+
const dispose = autorun2(
|
|
1085
|
+
() => {
|
|
1086
|
+
runInAction3(() => cleanup?.());
|
|
1087
|
+
cleanup = void 0;
|
|
1088
|
+
try {
|
|
1089
|
+
const result = fn();
|
|
1090
|
+
if (typeof result === "function") {
|
|
1091
|
+
cleanup = result;
|
|
1092
|
+
}
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
reportError(e, { phase: "effect", name: this.constructor.name, isBehavior: false });
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
{ delay: options?.delay }
|
|
1098
|
+
);
|
|
1099
|
+
return this._addCleanup(() => {
|
|
1100
|
+
runInAction3(() => cleanup?.());
|
|
1101
|
+
dispose();
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
303
1104
|
}
|
|
304
1105
|
/** @internal */
|
|
305
1106
|
_disposeWatchers() {
|
|
306
|
-
|
|
1107
|
+
const disposers = this._watchDisposers.slice();
|
|
1108
|
+
this._watchDisposers.length = 0;
|
|
1109
|
+
for (const dispose of disposers) {
|
|
307
1110
|
dispose();
|
|
308
1111
|
}
|
|
309
|
-
this._watchDisposers.length = 0;
|
|
310
1112
|
}
|
|
311
1113
|
/** @internal - Scan own properties for behavior instances and register them */
|
|
312
1114
|
_collectBehaviors() {
|
|
313
1115
|
for (const key of Object.keys(this)) {
|
|
314
1116
|
if (key.startsWith("_")) continue;
|
|
315
1117
|
const value = this[key];
|
|
316
|
-
if (isBehavior(value)) {
|
|
1118
|
+
if (isBehavior(value) && !isServiceValue(this, value)) {
|
|
317
1119
|
this._behaviors.push({ instance: value });
|
|
318
1120
|
}
|
|
319
1121
|
}
|
|
@@ -339,6 +1141,10 @@ var View = class {
|
|
|
339
1141
|
};
|
|
340
1142
|
var BASE_EXCLUDES = /* @__PURE__ */ new Set([
|
|
341
1143
|
"props",
|
|
1144
|
+
"getParent",
|
|
1145
|
+
"findParent",
|
|
1146
|
+
"getParents",
|
|
1147
|
+
"getRoot",
|
|
342
1148
|
"_propsBox",
|
|
343
1149
|
"forwardRef",
|
|
344
1150
|
"onCreate",
|
|
@@ -348,8 +1154,13 @@ var BASE_EXCLUDES = /* @__PURE__ */ new Set([
|
|
|
348
1154
|
"onUnmount",
|
|
349
1155
|
"render",
|
|
350
1156
|
"ref",
|
|
1157
|
+
"view",
|
|
1158
|
+
"addCleanup",
|
|
1159
|
+
"_addCleanup",
|
|
351
1160
|
"watch",
|
|
1161
|
+
"effect",
|
|
352
1162
|
"constructor",
|
|
1163
|
+
"inject",
|
|
353
1164
|
"_behaviors",
|
|
354
1165
|
"_collectBehaviors",
|
|
355
1166
|
"_layoutMountBehaviors",
|
|
@@ -357,12 +1168,16 @@ var BASE_EXCLUDES = /* @__PURE__ */ new Set([
|
|
|
357
1168
|
"_unmountBehaviors",
|
|
358
1169
|
"_syncProps",
|
|
359
1170
|
"_watchDisposers",
|
|
360
|
-
"_disposeWatchers"
|
|
1171
|
+
"_disposeWatchers",
|
|
1172
|
+
"_reactiveSpecs",
|
|
1173
|
+
"_mounted",
|
|
1174
|
+
"_wasUnmounted"
|
|
361
1175
|
]);
|
|
362
|
-
function
|
|
363
|
-
return value !== null && typeof value === "object" &&
|
|
1176
|
+
function isComponentRef(value) {
|
|
1177
|
+
return value !== null && typeof value === "object" && componentRefs.has(value);
|
|
364
1178
|
}
|
|
365
|
-
|
|
1179
|
+
var componentProtoInfo = /* @__PURE__ */ new WeakMap();
|
|
1180
|
+
function makeComponentObservable(instance, autoBind) {
|
|
366
1181
|
const annotations = {};
|
|
367
1182
|
const ownKeys = /* @__PURE__ */ new Set([
|
|
368
1183
|
...Object.keys(instance),
|
|
@@ -372,69 +1187,96 @@ function makeViewObservable(instance, autoBind) {
|
|
|
372
1187
|
if (BASE_EXCLUDES.has(key)) continue;
|
|
373
1188
|
if (key in annotations) continue;
|
|
374
1189
|
const value = instance[key];
|
|
375
|
-
if (typeof value === "function") continue;
|
|
1190
|
+
if (typeof value === "function" || isServiceValue(instance, value) || trackedComponents.has(value)) continue;
|
|
376
1191
|
if (isBehavior(value)) {
|
|
377
1192
|
annotations[key] = observable4.ref;
|
|
378
1193
|
continue;
|
|
379
1194
|
}
|
|
380
|
-
if (
|
|
1195
|
+
if (isComponentRef(value)) {
|
|
381
1196
|
annotations[key] = observable4.ref;
|
|
382
1197
|
} else {
|
|
383
1198
|
annotations[key] = observable4;
|
|
384
1199
|
}
|
|
385
1200
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1201
|
+
const protoInfo = collectProtoInfo(instance, Component.prototype, BASE_EXCLUDES, componentProtoInfo);
|
|
1202
|
+
for (const key of protoInfo.computedKeys) {
|
|
1203
|
+
if (key in annotations) continue;
|
|
1204
|
+
annotations[key] = computed4;
|
|
1205
|
+
}
|
|
1206
|
+
makeObservable2(instance, annotations);
|
|
1207
|
+
if (autoBind) {
|
|
1208
|
+
for (const key of protoInfo.methodKeys) {
|
|
391
1209
|
if (key in annotations) continue;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
annotations[key] = autoBind ? action4.bound : action4;
|
|
1210
|
+
const method = instance[key];
|
|
1211
|
+
if (typeof method === "function") {
|
|
1212
|
+
instance[key] = smartBind(method, instance);
|
|
396
1213
|
}
|
|
397
1214
|
}
|
|
398
|
-
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
var SubstituteComponent = class extends Component {
|
|
1218
|
+
};
|
|
1219
|
+
function installSubstituteState(instance, state) {
|
|
1220
|
+
const annotations = {};
|
|
1221
|
+
for (const key of Reflect.ownKeys(state)) {
|
|
1222
|
+
if (typeof key !== "string" || key.startsWith("_") || key in instance || BASE_EXCLUDES.has(key)) throw new Error("[mobx-mantle] Substitute state cannot replace framework member " + String(key));
|
|
1223
|
+
const descriptor = Object.getOwnPropertyDescriptor(state, key);
|
|
1224
|
+
if (!("value" in descriptor)) throw new Error("[mobx-mantle] Substitute state must contain data/callbacks, not getters.");
|
|
1225
|
+
Object.defineProperty(instance, key, { value: descriptor.value, enumerable: true, writable: true, configurable: true });
|
|
1226
|
+
annotations[key] = observable4.ref;
|
|
399
1227
|
}
|
|
400
1228
|
makeObservable2(instance, annotations);
|
|
401
1229
|
}
|
|
402
|
-
function
|
|
1230
|
+
function createComponent(ComponentClass, templateOrOptions) {
|
|
403
1231
|
const template = typeof templateOrOptions === "function" ? templateOrOptions : void 0;
|
|
404
1232
|
const options = typeof templateOrOptions === "object" ? templateOrOptions : {};
|
|
405
1233
|
const { autoObservable = globalConfig.autoObservable } = options;
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
1234
|
+
const ReactComponent = reactForwardRef((props, ref) => {
|
|
1235
|
+
const parentHandle = React4.useContext(ComponentParentContext);
|
|
1236
|
+
const resolver = React4.useContext(ServiceContext);
|
|
1237
|
+
const substitutions = React4.useContext(ModelContext);
|
|
1238
|
+
const substitutedTemplate = useRef2(void 0);
|
|
1239
|
+
const vmRef = useRef2(null);
|
|
1240
|
+
const componentHandleRef = useRef2(null);
|
|
1241
|
+
const classRef = useRef2(ComponentClass);
|
|
1242
|
+
const prevPropsRef = useRef2(null);
|
|
1243
|
+
const propsNotifyingRef = useRef2(false);
|
|
1244
|
+
const renderReactionRef = useRef2({ current: null });
|
|
1245
|
+
if (!componentHandleRef.current) {
|
|
1246
|
+
componentHandleRef.current = { current: void 0 };
|
|
1247
|
+
}
|
|
1248
|
+
const componentHandle = componentHandleRef.current;
|
|
1249
|
+
if (vmRef.current && classRef.current !== ComponentClass) {
|
|
1250
|
+
classRef.current = ComponentClass;
|
|
413
1251
|
vmRef.current = null;
|
|
414
1252
|
}
|
|
415
1253
|
if (!vmRef.current) {
|
|
416
|
-
|
|
417
|
-
|
|
1254
|
+
applyMobxActionPolicy();
|
|
1255
|
+
const substitution = substitutions?.get(ComponentClass);
|
|
1256
|
+
if (substitution && !substitution.template && !template) throw new Error("[mobx-mantle] A substituted integrated model requires an explicit test template.");
|
|
1257
|
+
const instance = withInjectionScope(resolver, () => substitution ? constructComponent(SubstituteComponent, props, parentHandle) : constructComponent(ComponentClass, props, parentHandle));
|
|
1258
|
+
substitutedTemplate.current = substitution?.template;
|
|
1259
|
+
setModelIdentity(instance, ComponentClass);
|
|
1260
|
+
if (substitution) installSubstituteState(instance, substitution.state());
|
|
418
1261
|
instance.forwardRef = ref;
|
|
419
|
-
instance.
|
|
1262
|
+
instance._propsBox._renderReaction = renderReactionRef.current;
|
|
1263
|
+
ownerScopes.set(instance, { handle: componentHandle, resolver: getServiceResolver(instance), substitutions });
|
|
1264
|
+
if (!substitution) instance._collectBehaviors();
|
|
420
1265
|
const decoratorAnnotations = getAnnotations(instance);
|
|
421
|
-
if (
|
|
1266
|
+
if (substitution) {
|
|
1267
|
+
} else if (decoratorAnnotations) {
|
|
422
1268
|
const annotations = { ...decoratorAnnotations };
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
annotations[key] = action4.bound;
|
|
431
|
-
}
|
|
1269
|
+
makeObservable2(instance, annotations);
|
|
1270
|
+
const protoInfo = collectProtoInfo(instance, Component.prototype, BASE_EXCLUDES, componentProtoInfo);
|
|
1271
|
+
for (const key of protoInfo.methodKeys) {
|
|
1272
|
+
if (key in annotations) continue;
|
|
1273
|
+
const method = instance[key];
|
|
1274
|
+
if (typeof method === "function") {
|
|
1275
|
+
instance[key] = smartBind(method, instance);
|
|
432
1276
|
}
|
|
433
|
-
proto = Object.getPrototypeOf(proto);
|
|
434
1277
|
}
|
|
435
|
-
makeObservable2(instance, annotations);
|
|
436
1278
|
} else if (autoObservable) {
|
|
437
|
-
|
|
1279
|
+
makeComponentObservable(instance, true);
|
|
438
1280
|
} else {
|
|
439
1281
|
makeObservable2(instance);
|
|
440
1282
|
}
|
|
@@ -444,99 +1286,283 @@ function createView(ViewClass, templateOrOptions) {
|
|
|
444
1286
|
ownKeys: () => Reflect.ownKeys(instance.props),
|
|
445
1287
|
getOwnPropertyDescriptor: (_, key) => Reflect.getOwnPropertyDescriptor(instance.props, key)
|
|
446
1288
|
});
|
|
447
|
-
instance.onCreate?.(reactiveProps);
|
|
1289
|
+
withOwnerServiceScope(instance, () => runInAction3(() => instance.onCreate?.(reactiveProps)));
|
|
1290
|
+
if (process.env.NODE_ENV !== "production" && autoObservable && !decoratorAnnotations && !substitution) {
|
|
1291
|
+
for (const key of Object.keys(instance)) {
|
|
1292
|
+
if (BASE_EXCLUDES.has(key) || key.startsWith("_")) continue;
|
|
1293
|
+
const value = instance[key];
|
|
1294
|
+
if (typeof value === "function" || isServiceValue(instance, value) || trackedComponents.has(value)) continue;
|
|
1295
|
+
if (!isObservableProp(instance, key)) {
|
|
1296
|
+
console.warn(
|
|
1297
|
+
`[mobx-mantle] ${ComponentClass.name}.${key} was first assigned in onCreate() and is not reactive. Declare it as a class field so it can be made observable.`
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
448
1302
|
vmRef.current = instance;
|
|
449
1303
|
prevPropsRef.current = props;
|
|
450
1304
|
}
|
|
451
1305
|
const vm = vmRef.current;
|
|
1306
|
+
const renderTemplate = substitutedTemplate.current ?? template;
|
|
1307
|
+
componentParents.set(vm, parentHandle);
|
|
1308
|
+
componentHandle.current = vm;
|
|
452
1309
|
if (process.env.NODE_ENV !== "production" && propsNotifyingRef.current) {
|
|
453
1310
|
console.warn(
|
|
454
|
-
`[mobx-mantle] ${
|
|
1311
|
+
`[mobx-mantle] ${ComponentClass.name}: A reaction to a prop change modified observable state, which caused an extra re-render. Consider using a computed getter instead.`
|
|
455
1312
|
);
|
|
456
1313
|
propsNotifyingRef.current = false;
|
|
457
1314
|
}
|
|
458
1315
|
vm._syncProps(props);
|
|
459
1316
|
vm.forwardRef = ref;
|
|
460
|
-
|
|
1317
|
+
useIsomorphicLayoutEffect(() => {
|
|
461
1318
|
if (!shallowEqual(prevPropsRef.current, props)) {
|
|
462
1319
|
prevPropsRef.current = props;
|
|
463
1320
|
propsNotifyingRef.current = true;
|
|
464
|
-
|
|
1321
|
+
runInAction3(() => {
|
|
465
1322
|
vm._propsBox.set(props);
|
|
466
1323
|
});
|
|
467
1324
|
propsNotifyingRef.current = false;
|
|
468
1325
|
}
|
|
469
1326
|
});
|
|
470
|
-
|
|
1327
|
+
useIsomorphicLayoutEffect(() => {
|
|
1328
|
+
activateSpecs(vm);
|
|
471
1329
|
vm._layoutMountBehaviors();
|
|
472
1330
|
let cleanup;
|
|
473
1331
|
try {
|
|
474
|
-
const result = vm.onLayoutMount?.();
|
|
1332
|
+
const result = runInAction3(() => vm.onLayoutMount?.());
|
|
475
1333
|
if (process.env.NODE_ENV !== "production" && result instanceof Promise) {
|
|
476
1334
|
console.error(
|
|
477
|
-
`[mobx-mantle] ${
|
|
1335
|
+
`[mobx-mantle] ${ComponentClass.name}.onLayoutMount() returned a Promise. Lifecycle methods must be synchronous. Use a sync onLayoutMount that calls an async method instead.`
|
|
478
1336
|
);
|
|
479
1337
|
}
|
|
480
|
-
cleanup = result;
|
|
1338
|
+
cleanup = typeof result === "function" ? result : void 0;
|
|
481
1339
|
} catch (e) {
|
|
482
|
-
reportError(e, { phase: "onLayoutMount", name:
|
|
1340
|
+
reportError(e, { phase: "onLayoutMount", name: ComponentClass.name, isBehavior: false });
|
|
483
1341
|
}
|
|
484
1342
|
return () => {
|
|
485
|
-
cleanup?.();
|
|
1343
|
+
runInAction3(() => cleanup?.());
|
|
486
1344
|
};
|
|
487
1345
|
}, [vm]);
|
|
488
|
-
|
|
1346
|
+
useEffect2(() => {
|
|
489
1347
|
vm._mountBehaviors();
|
|
490
1348
|
let cleanup;
|
|
491
1349
|
try {
|
|
492
|
-
const result = vm.onMount?.();
|
|
1350
|
+
const result = runInAction3(() => vm.onMount?.());
|
|
493
1351
|
if (process.env.NODE_ENV !== "production" && result instanceof Promise) {
|
|
494
1352
|
console.error(
|
|
495
|
-
`[mobx-mantle] ${
|
|
1353
|
+
`[mobx-mantle] ${ComponentClass.name}.onMount() returned a Promise. Lifecycle methods must be synchronous. Use a sync onMount that calls an async method instead.`
|
|
496
1354
|
);
|
|
497
1355
|
}
|
|
498
|
-
cleanup = result;
|
|
1356
|
+
cleanup = typeof result === "function" ? result : void 0;
|
|
499
1357
|
} catch (e) {
|
|
500
|
-
reportError(e, { phase: "onMount", name:
|
|
1358
|
+
reportError(e, { phase: "onMount", name: ComponentClass.name, isBehavior: false });
|
|
1359
|
+
}
|
|
1360
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1361
|
+
warnUncollectedBehaviors(vm, ComponentClass.name, vm._behaviors);
|
|
501
1362
|
}
|
|
502
1363
|
return () => {
|
|
503
|
-
cleanup?.();
|
|
1364
|
+
runInAction3(() => cleanup?.());
|
|
504
1365
|
try {
|
|
505
|
-
vm.onUnmount?.();
|
|
1366
|
+
runInAction3(() => vm.onUnmount?.());
|
|
506
1367
|
} catch (e) {
|
|
507
|
-
reportError(e, { phase: "onUnmount", name:
|
|
1368
|
+
reportError(e, { phase: "onUnmount", name: ComponentClass.name, isBehavior: false });
|
|
508
1369
|
}
|
|
509
1370
|
vm._disposeWatchers();
|
|
510
1371
|
vm._unmountBehaviors();
|
|
1372
|
+
vm._wasUnmounted = true;
|
|
511
1373
|
};
|
|
512
1374
|
}, [vm]);
|
|
513
|
-
|
|
1375
|
+
useEffect2(() => {
|
|
514
1376
|
try {
|
|
515
|
-
vm.onUpdate?.();
|
|
1377
|
+
runInAction3(() => vm.onUpdate?.());
|
|
516
1378
|
} catch (e) {
|
|
517
|
-
reportError(e, { phase: "onUpdate", name:
|
|
1379
|
+
reportError(e, { phase: "onUpdate", name: ComponentClass.name, isBehavior: false });
|
|
518
1380
|
}
|
|
519
1381
|
});
|
|
520
|
-
if (!
|
|
1382
|
+
if (!renderTemplate && !vm.render) {
|
|
521
1383
|
throw new Error(
|
|
522
|
-
`[mobx-mantle] ${
|
|
1384
|
+
`[mobx-mantle] ${ComponentClass.name}: Missing render() method. Either define render() in your Component class or pass a template function to createComponent().`
|
|
523
1385
|
);
|
|
524
1386
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
1387
|
+
const rendered = useMantleObserver(
|
|
1388
|
+
() => renderTemplate ? renderTemplate(vm) : vm.render(),
|
|
1389
|
+
ComponentClass.name,
|
|
1390
|
+
renderReactionRef.current
|
|
1391
|
+
);
|
|
1392
|
+
return /* @__PURE__ */ jsx3(ComponentParentContext.Provider, { value: componentHandle, children: rendered });
|
|
528
1393
|
});
|
|
529
|
-
return
|
|
1394
|
+
return memo2(ReactComponent);
|
|
1395
|
+
}
|
|
1396
|
+
function createForwardRef(ComponentClass, templateOrOptions) {
|
|
1397
|
+
return createComponent(ComponentClass, templateOrOptions);
|
|
1398
|
+
}
|
|
1399
|
+
function createView(ModelClass, template) {
|
|
1400
|
+
const name = `${ModelClass.name}.${template.name || "view"}`;
|
|
1401
|
+
const View = reactForwardRef((props, ref) => {
|
|
1402
|
+
let vm = useContext(ComponentParentContext)?.current;
|
|
1403
|
+
while (vm && !isModel(vm, ModelClass)) vm = vm.getParent();
|
|
1404
|
+
if (!vm) {
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
`[mobx-mantle] createView(${ModelClass.name}): no ${ModelClass.name} is mounted above this view.`
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
const model = vm;
|
|
1410
|
+
return useMantleObserver(() => template(withRef({ ...props, vm: model }, ref)), name);
|
|
1411
|
+
});
|
|
1412
|
+
View.displayName = name;
|
|
1413
|
+
const Memoized = memo2(View);
|
|
1414
|
+
trackedComponents.add(Memoized);
|
|
1415
|
+
return Memoized;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/useBehavior.ts
|
|
1419
|
+
import { useContext as useContext2 } from "react";
|
|
1420
|
+
import { useEffect as useEffect3, useState } from "react";
|
|
1421
|
+
function useBehavior(create) {
|
|
1422
|
+
const resolver = useContext2(ServiceContext);
|
|
1423
|
+
const [entry] = useState(() => {
|
|
1424
|
+
const instance = withInjectionScope(resolver, create);
|
|
1425
|
+
if (process.env.NODE_ENV !== "production" && !isBehavior(instance)) {
|
|
1426
|
+
console.warn(
|
|
1427
|
+
"[mobx-mantle] useBehavior() expected the factory to return a behavior instance (created with a factory from createBehavior). Lifecycle relay will be skipped for this value."
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
return { instance };
|
|
1431
|
+
});
|
|
1432
|
+
useIsomorphicLayoutEffect(() => {
|
|
1433
|
+
if (!isBehavior(entry.instance)) return;
|
|
1434
|
+
layoutMountBehavior(entry);
|
|
1435
|
+
}, [entry]);
|
|
1436
|
+
useEffect3(() => {
|
|
1437
|
+
if (!isBehavior(entry.instance)) return;
|
|
1438
|
+
mountBehavior(entry);
|
|
1439
|
+
return () => unmountBehavior(entry);
|
|
1440
|
+
}, [entry]);
|
|
1441
|
+
return entry.instance;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/shared.ts
|
|
1445
|
+
import { isObservableProp as isObservableProp2, reaction as reaction3, runInAction as runInAction4, toJS } from "mobx";
|
|
1446
|
+
var registry = /* @__PURE__ */ new WeakMap();
|
|
1447
|
+
function ownDataFields(store) {
|
|
1448
|
+
return Object.keys(store).filter((key) => {
|
|
1449
|
+
if (key.startsWith("_")) return false;
|
|
1450
|
+
if (typeof store[key] === "function") return false;
|
|
1451
|
+
return isObservableProp2(store, key);
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
function serialize(store, fields) {
|
|
1455
|
+
const values = {};
|
|
1456
|
+
for (const field of fields) values[field] = toJS(store[field]);
|
|
1457
|
+
return JSON.stringify(values);
|
|
1458
|
+
}
|
|
1459
|
+
function apply(store, fields, json) {
|
|
1460
|
+
let parsed;
|
|
1461
|
+
try {
|
|
1462
|
+
parsed = JSON.parse(json);
|
|
1463
|
+
} catch {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
1467
|
+
const values = parsed;
|
|
1468
|
+
runInAction4(() => {
|
|
1469
|
+
for (const field of fields) {
|
|
1470
|
+
if (field in values) store[field] = values[field];
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
function safeStorage() {
|
|
1476
|
+
try {
|
|
1477
|
+
return typeof localStorage === "undefined" ? null : localStorage;
|
|
1478
|
+
} catch {
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function shared(store, key, options = {}) {
|
|
1483
|
+
if (registry.has(store) || typeof window === "undefined") return store;
|
|
1484
|
+
const target = store;
|
|
1485
|
+
const fields = options.fields ? [...options.fields] : ownDataFields(store);
|
|
1486
|
+
const storage = options.persist === false ? null : safeStorage();
|
|
1487
|
+
const channel = typeof BroadcastChannel === "function" ? new BroadcastChannel(`mantle-shared:${key}`) : null;
|
|
1488
|
+
channel?.unref?.();
|
|
1489
|
+
let stored = null;
|
|
1490
|
+
try {
|
|
1491
|
+
stored = storage?.getItem(key) ?? null;
|
|
1492
|
+
} catch {
|
|
1493
|
+
stored = null;
|
|
1494
|
+
}
|
|
1495
|
+
if (stored !== null) apply(target, fields, stored);
|
|
1496
|
+
let current = serialize(target, fields);
|
|
1497
|
+
const stopPublish = reaction3(
|
|
1498
|
+
() => serialize(target, fields),
|
|
1499
|
+
(json) => {
|
|
1500
|
+
if (json === current) return;
|
|
1501
|
+
current = json;
|
|
1502
|
+
try {
|
|
1503
|
+
storage?.setItem(key, json);
|
|
1504
|
+
} catch {
|
|
1505
|
+
}
|
|
1506
|
+
channel?.postMessage(json);
|
|
1507
|
+
}
|
|
1508
|
+
);
|
|
1509
|
+
const receive = (json) => {
|
|
1510
|
+
if (json === current) return;
|
|
1511
|
+
current = json;
|
|
1512
|
+
apply(target, fields, json);
|
|
1513
|
+
};
|
|
1514
|
+
const onMessage = (event) => {
|
|
1515
|
+
if (typeof event.data === "string") receive(event.data);
|
|
1516
|
+
};
|
|
1517
|
+
const onStorage = (event) => {
|
|
1518
|
+
if (event.key === key && event.newValue !== null) receive(event.newValue);
|
|
1519
|
+
};
|
|
1520
|
+
channel?.addEventListener("message", onMessage);
|
|
1521
|
+
window.addEventListener("storage", onStorage);
|
|
1522
|
+
registry.set(store, {
|
|
1523
|
+
dispose() {
|
|
1524
|
+
stopPublish();
|
|
1525
|
+
channel?.removeEventListener("message", onMessage);
|
|
1526
|
+
channel?.close();
|
|
1527
|
+
window.removeEventListener("storage", onStorage);
|
|
1528
|
+
registry.delete(store);
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
return store;
|
|
1532
|
+
}
|
|
1533
|
+
function unshare(store) {
|
|
1534
|
+
registry.get(store)?.dispose();
|
|
1535
|
+
}
|
|
1536
|
+
function shareStores(stores, options = {}) {
|
|
1537
|
+
const { prefix = "shared", ...rest } = options;
|
|
1538
|
+
for (const [name, store] of Object.entries(stores)) {
|
|
1539
|
+
shared(store, `${prefix}:${name}`, rest);
|
|
1540
|
+
}
|
|
1541
|
+
return stores;
|
|
530
1542
|
}
|
|
531
1543
|
export {
|
|
532
1544
|
Behavior,
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1545
|
+
Component,
|
|
1546
|
+
ModelProvider,
|
|
1547
|
+
ServiceProvider,
|
|
1548
|
+
Component as ViewModel,
|
|
1549
|
+
action2 as action,
|
|
1550
|
+
computed2 as computed,
|
|
537
1551
|
configure,
|
|
538
1552
|
createBehavior,
|
|
1553
|
+
createComponent,
|
|
1554
|
+
createForwardRef,
|
|
1555
|
+
createServiceToken,
|
|
539
1556
|
createView,
|
|
540
|
-
|
|
1557
|
+
inject,
|
|
1558
|
+
isModel,
|
|
1559
|
+
observable2 as observable,
|
|
1560
|
+
observer,
|
|
1561
|
+
shareStores,
|
|
1562
|
+
shared,
|
|
1563
|
+
toValue,
|
|
1564
|
+
unshare,
|
|
1565
|
+
useBehavior,
|
|
1566
|
+
withInjectionScope
|
|
541
1567
|
};
|
|
542
1568
|
//# sourceMappingURL=index.js.map
|