react-deepwatch 1.0.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/LICENSE +21 -0
- package/Util.d.ts +66 -0
- package/Util.d.ts.map +1 -0
- package/Util.js +178 -0
- package/Util.js.map +1 -0
- package/Util.ts +190 -0
- package/dist/mjs/Util.d.ts +66 -0
- package/dist/mjs/Util.d.ts.map +1 -0
- package/dist/mjs/Util.js +164 -0
- package/dist/mjs/Util.js.map +1 -0
- package/dist/mjs/index.d.ts +218 -0
- package/dist/mjs/index.d.ts.map +1 -0
- package/dist/mjs/index.js +821 -0
- package/dist/mjs/index.js.map +1 -0
- package/dist/mjs/preserve.d.ts +72 -0
- package/dist/mjs/preserve.d.ts.map +1 -0
- package/dist/mjs/preserve.js +374 -0
- package/dist/mjs/preserve.js.map +1 -0
- package/index.d.ts +218 -0
- package/index.d.ts.map +1 -0
- package/index.js +830 -0
- package/index.js.map +1 -0
- package/index.ts +1237 -0
- package/index_esm.mjs +6 -0
- package/mechanics.md +47 -0
- package/package.json +59 -0
- package/preserve.d.ts +72 -0
- package/preserve.d.ts.map +1 -0
- package/preserve.js +385 -0
- package/preserve.js.map +1 -0
- package/preserve.ts +492 -0
- package/readme.md +109 -0
package/index.ts
ADDED
|
@@ -0,0 +1,1237 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RecordedRead,
|
|
3
|
+
RecordedReadOnProxiedObject,
|
|
4
|
+
RecordedValueRead,
|
|
5
|
+
WatchedProxyFacade, installChangeTracker
|
|
6
|
+
} from "proxy-facades";
|
|
7
|
+
import {arraysAreEqualsByPredicateFn, isObject, PromiseState, recordedReadsArraysAreEqual, throwError} from "./Util";
|
|
8
|
+
import {useLayoutEffect, useState, createElement, Fragment, ReactNode, useEffect, useContext, memo} from "react";
|
|
9
|
+
import {ErrorBoundaryContext, useErrorBoundary} from "react-error-boundary";
|
|
10
|
+
import {_preserve, preserve, PreserveOptions} from "./preserve";
|
|
11
|
+
|
|
12
|
+
let watchedProxyFacade: WatchedProxyFacade | undefined
|
|
13
|
+
function getWatchedProxyFacade() {
|
|
14
|
+
return watchedProxyFacade || (watchedProxyFacade = new WatchedProxyFacade()); // Lazy initialize global variable
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let debug_idGenerator=0;
|
|
18
|
+
|
|
19
|
+
type WatchedComponentOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* A fallback react tree to show when some `load(...)` statement in <strong>this</strong> component is loading.
|
|
22
|
+
* Use this if you have issues with screen flickering with <code><Suspense></code>.
|
|
23
|
+
*/
|
|
24
|
+
fallback?: ReactNode,
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wraps this component in a {@link https://react.dev/reference/react/memo memo} to prevent unnecessary re-renders.
|
|
28
|
+
* This is enabled by default, since watchedComponents smartly tracks deep changes of used props and knows when to rerender.
|
|
29
|
+
* Disable this only in a mixed scenario with non-watchedComponents, where they rely on the old way of fully re-rendering the whole tree to pass deep model data (=more than using shallow, primitive props) to the leaves. So this component does not block these re-renders in the middle.
|
|
30
|
+
* <p>
|
|
31
|
+
* Default: true
|
|
32
|
+
* </p>
|
|
33
|
+
*/
|
|
34
|
+
memo?: boolean
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* TODO
|
|
38
|
+
* Normally, everything that's **taken** from props, {@link useWatchedState} or {@link watched} or load(...)'s result will be returned, wrapped in a proxy that watches for modifications.
|
|
39
|
+
* So far, so good, this can handle all stuff that's happening inside your component, but the outside world does not have these proxies. For example, when a parent component is not a watchedComponent, and passed in an object (i.e. the model) into this component via props.
|
|
40
|
+
* Therefore this component can also **patch** these objects to make them watchable.
|
|
41
|
+
*
|
|
42
|
+
*
|
|
43
|
+
* <p>Default: true</p>
|
|
44
|
+
*/
|
|
45
|
+
watchOutside?: boolean
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* TODO: implement
|
|
49
|
+
* Preserves object instances in props by running the {@link preserve} function over the props (where the last value is memoized).
|
|
50
|
+
* <p>
|
|
51
|
+
* It's not recommended to enable this. Use only as a workaround when working with non-watched components where you have no control over. Better run {@link preserve} on the fetched source and keep a consistent-instance model in your app in the first place.
|
|
52
|
+
* </p>
|
|
53
|
+
*
|
|
54
|
+
* Note: Even with false, the `props` root object still keeps its instance (so it's save to watch `props.myFirstLevelProperty`).
|
|
55
|
+
* <p>
|
|
56
|
+
* Default: false
|
|
57
|
+
* </p>
|
|
58
|
+
*/
|
|
59
|
+
preserveProps?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Contains the preconditions and the state / polling state for a load(...) statement.
|
|
64
|
+
* Very volatile. Will be invalid as soon as a precondition changes, or if it's not used or currently not reachable (in that case there will spawn another LoadRun).
|
|
65
|
+
*/
|
|
66
|
+
class LoadRun {
|
|
67
|
+
debug_id = ++debug_idGenerator;
|
|
68
|
+
|
|
69
|
+
loadCall: LoadCall
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reference may be forgotten, when not needed for re-polling
|
|
74
|
+
*/
|
|
75
|
+
loaderFn?: (oldResult?: unknown) => Promise<unknown>;
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* From the beginning or previous load call up to this one
|
|
80
|
+
*/
|
|
81
|
+
recordedReadsBefore!: RecordedRead[];
|
|
82
|
+
recordedReadsInsideLoaderFn!: RecordedRead[];
|
|
83
|
+
|
|
84
|
+
result!: PromiseState<unknown>;
|
|
85
|
+
|
|
86
|
+
lastExecTime?: Date;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Result from setTimeout.
|
|
90
|
+
* Set, when re-polling is scheduled or running (=the loaderFn is re-running)
|
|
91
|
+
*/
|
|
92
|
+
rePollTimer?: any;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* index in this.watchedComponentPersistent.loadRuns
|
|
96
|
+
*/
|
|
97
|
+
cache_index: number;
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
get isObsolete() {
|
|
101
|
+
return !(this.loadCall.watchedComponentPersistent.loadRuns.length > this.cache_index && this.loadCall.watchedComponentPersistent.loadRuns[this.cache_index] === this); // this.watchedComponentPersistent.loadRuns does not contain this?
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get options() {
|
|
105
|
+
return this.loadCall.options;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get name() {
|
|
109
|
+
return this.options.name;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async exec() {
|
|
113
|
+
try {
|
|
114
|
+
if(this.options.fixedInterval !== false) this.lastExecTime = new Date(); // Take timestamp
|
|
115
|
+
const lastResult = this.loadCall.lastResult;
|
|
116
|
+
let result = await this.loaderFn!(lastResult);
|
|
117
|
+
if(this.options.preserve !== false) { // Preserve enabled?
|
|
118
|
+
if(isObject(result)) { // Result is mergeable ?
|
|
119
|
+
this.loadCall.isUniquelyIdentified() || throwError(new Error(`Please specify a key via load(..., { key:<your key> }), so the result's object identity can be preserved. See LoadOptions#key and LoadOptions#preserve. Look at the cause to see where load(...) was called`, {cause: this.loadCall.diagnosis_callstack}));
|
|
120
|
+
const preserveOptions = (typeof this.options.preserve === "object")?this.options.preserve: {};
|
|
121
|
+
result = _preserve(lastResult,result, preserveOptions, {callStack: this.loadCall.diagnosis_callstack});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Save lastresult:
|
|
126
|
+
if(this.options.preserve !== false || this.options.silent) { // last result will be needed later?
|
|
127
|
+
this.loadCall.lastResult = result; // save for later
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Be memory friendly and don't leak references.
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if(this.options.fixedInterval === false) this.lastExecTime = new Date(); // Take timestamp
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
activateRegularRePollingIfNeeded() {
|
|
141
|
+
// Check, if we should really schedule:
|
|
142
|
+
this.checkValid();
|
|
143
|
+
if(!this.options.interval) { // Polling not enabled ?
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if(this.rePollTimer !== undefined) { // Already scheduled ?
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if(this.isObsolete) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if(this.result.state === "pending") {
|
|
153
|
+
return; // will call activateRegularRePollingIfNeeded() when load is finished and a rerender is done
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.rePollTimer = setTimeout(async () => {
|
|
157
|
+
// Check, if we should really execute:
|
|
158
|
+
this.checkValid();
|
|
159
|
+
if (this.rePollTimer === undefined) { // Not scheduled anymore / frame not alive?
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (this.isObsolete) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await this.executeRePoll();
|
|
167
|
+
|
|
168
|
+
// Now that some time may have passed, check, again, if we should really schedule the next poll:
|
|
169
|
+
this.checkValid();
|
|
170
|
+
if (this.rePollTimer === undefined) { // Not scheduled anymore / frame not alive?
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (this.isObsolete) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Re-schedule
|
|
178
|
+
clearTimeout(this.rePollTimer); // Call this to make sure...May be polling has been activated and deactivated in the manwhile during executeRePoll and this.rePollTimer is now another one
|
|
179
|
+
this.rePollTimer = undefined;
|
|
180
|
+
this.activateRegularRePollingIfNeeded();
|
|
181
|
+
}, Math.max(0, this.options.interval - (new Date().getTime() - this.lastExecTime!.getTime())) );
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Re runs loaderFn
|
|
186
|
+
*/
|
|
187
|
+
async executeRePoll() {
|
|
188
|
+
try {
|
|
189
|
+
const value = await this.exec();
|
|
190
|
+
const isChanged = !(this.result.state === "resolved" && value === this.result.resolvedValue)
|
|
191
|
+
this.result = {state: "resolved", resolvedValue:value}
|
|
192
|
+
|
|
193
|
+
if(this.isObsolete) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if(isChanged) {
|
|
198
|
+
this.loadCall.watchedComponentPersistent.handleChangeEvent(); // requests a re-render
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if(this.options.critical === false && isObject(value)) {
|
|
202
|
+
this.loadCall.watchedComponentPersistent.requestReRender(); // Non-critical objects are not watched. But their deep changed content is used in the render. I.e. <div>{ load(() => {return {msg: `counter: ...`}}, {critical:false}).msg }</div>
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
this.result = {state: "rejected", rejectReason: e};
|
|
208
|
+
if(!this.isObsolete) {
|
|
209
|
+
this.loadCall.watchedComponentPersistent.handleChangeEvent();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
deactivateRegularRePoll() {
|
|
215
|
+
this.checkValid();
|
|
216
|
+
if(this.rePollTimer !== undefined) {
|
|
217
|
+
clearTimeout(this.rePollTimer);
|
|
218
|
+
this.rePollTimer = undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
checkValid() {
|
|
223
|
+
if(this.rePollTimer !== undefined && this.result.state === "pending") {
|
|
224
|
+
throw new Error("Illegal state");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
constructor(loadCall: LoadCall, loaderFn: LoadRun["loaderFn"], cache_index: number) {
|
|
230
|
+
this.loadCall = loadCall;
|
|
231
|
+
this.loaderFn = loaderFn;
|
|
232
|
+
this.cache_index = cache_index;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Uniquely identifies a call, to remember the lastResult to preserve object instances
|
|
238
|
+
*/
|
|
239
|
+
class LoadCall {
|
|
240
|
+
/**
|
|
241
|
+
* Unique id. (Source can be determined through the call stack), or, if run in a loop, it must be specified by the user
|
|
242
|
+
*/
|
|
243
|
+
id?: string | number | object;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Back reference to it
|
|
247
|
+
*/
|
|
248
|
+
watchedComponentPersistent: WatchedComponentPersistent;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
*
|
|
252
|
+
*/
|
|
253
|
+
options!: LoadOptions & Partial<PollOptions>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Fore preserving
|
|
257
|
+
*/
|
|
258
|
+
lastResult: unknown
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
diagnosis_callstack?: Error
|
|
262
|
+
diagnosis_callerSourceLocation?: string
|
|
263
|
+
|
|
264
|
+
constructor(id: LoadCall["id"], watchedComponentPersistent: WatchedComponentPersistent, options: LoadOptions & Partial<PollOptions>, diagnosis_callStack: Error | undefined, diagnosis_callerSourceLocation: string | undefined) {
|
|
265
|
+
this.id = id;
|
|
266
|
+
this.watchedComponentPersistent = watchedComponentPersistent;
|
|
267
|
+
this.options = options;
|
|
268
|
+
this.diagnosis_callstack = diagnosis_callStack;
|
|
269
|
+
this.diagnosis_callerSourceLocation = diagnosis_callerSourceLocation
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
isUniquelyIdentified() {
|
|
273
|
+
let registeredForId = this.watchedComponentPersistent.loadCalls.get(this.id);
|
|
274
|
+
if(registeredForId === undefined) {
|
|
275
|
+
throw new Error("Illegal state: No Load call for this id was registered");
|
|
276
|
+
}
|
|
277
|
+
if(registeredForId === null) {
|
|
278
|
+
return false; // Null means: Not unique
|
|
279
|
+
}
|
|
280
|
+
if(registeredForId !== this) {
|
|
281
|
+
throw new Error("Illegal state: A different load call for this id was registered.");
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Value from the {@link LoadOptions#fallback} or through the {@link LoadOptions#silent} mechanism.
|
|
288
|
+
* Undefined, when no such "fallback" is available
|
|
289
|
+
*/
|
|
290
|
+
getFallbackValue(): {value: unknown} | undefined {
|
|
291
|
+
!(this.options.silent && !this.isUniquelyIdentified()) || throwError(`Please specify a key via load(..., { key:<your key> }), to allow LoadOptions#silent to re-identify the last result. See LoadOptions#key and LoadOptions#silent.`); // Validity check
|
|
292
|
+
|
|
293
|
+
if(this.options.silent && this.lastResult !== undefined) {
|
|
294
|
+
return {value: this.lastResult}
|
|
295
|
+
}
|
|
296
|
+
else if(this.options.hasOwnProperty("fallback")) {
|
|
297
|
+
return {value: this.options.fallback};
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Fields that persist across re-render and across frames
|
|
305
|
+
*/
|
|
306
|
+
class WatchedComponentPersistent {
|
|
307
|
+
options: WatchedComponentOptions;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* props of the component. These are saved here in the state (in a non changing object instance), so code inside load call can watch **shallow** props changes on it.
|
|
311
|
+
*/
|
|
312
|
+
watchedProps = getWatchedProxyFacade().getProxyFor({});
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* id -> loadCall. Null when there are multiple for that id
|
|
316
|
+
*/
|
|
317
|
+
loadCalls = new Map<LoadCall["id"], LoadCall | null>();
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* LoadRuns in the exact order, they occur
|
|
321
|
+
*/
|
|
322
|
+
loadRuns: LoadRun[] = [];
|
|
323
|
+
|
|
324
|
+
currentFrame?: Frame
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* - true = rerender requested (will re-render asap) or just starting the render and changes in props/state/watched still make it into it.
|
|
328
|
+
* - false = ...
|
|
329
|
+
* - RenderRun = A passive render is requested. Save reference to the render run as safety check
|
|
330
|
+
*/
|
|
331
|
+
reRenderRequested: boolean | RenderRun = false;
|
|
332
|
+
|
|
333
|
+
_doReRender!: () => void
|
|
334
|
+
|
|
335
|
+
hadASuccessfullMount = false;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Called either before or on the render
|
|
339
|
+
*/
|
|
340
|
+
onceOnReRenderListeners: (()=>void)[] = [];
|
|
341
|
+
|
|
342
|
+
onceOnEffectCleanupListeners: (()=>void)[] = [];
|
|
343
|
+
|
|
344
|
+
debug_tag?: string;
|
|
345
|
+
|
|
346
|
+
protected doReRender() {
|
|
347
|
+
// Call listeners:
|
|
348
|
+
this.onceOnReRenderListeners.forEach(fn => fn());
|
|
349
|
+
this.onceOnReRenderListeners = [];
|
|
350
|
+
|
|
351
|
+
this._doReRender()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
requestReRender(passiveFromRenderRun?: RenderRun) {
|
|
355
|
+
const wasAlreadyRequested = this.reRenderRequested !== false;
|
|
356
|
+
|
|
357
|
+
// Enable the reRenderRequested flag:
|
|
358
|
+
if(passiveFromRenderRun !== undefined && this.reRenderRequested !== true) {
|
|
359
|
+
this.reRenderRequested = passiveFromRenderRun;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
this.reRenderRequested = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if(wasAlreadyRequested) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Do the re-render:
|
|
370
|
+
if(currentRenderRun !== undefined) {
|
|
371
|
+
// Must defer it because we cannot call rerender from inside a render function
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
this.doReRender();
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
this.doReRender();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* When a load finished or finished with error, or when a watched value changed. So the component needs to be rerendered
|
|
383
|
+
*/
|
|
384
|
+
handleChangeEvent() {
|
|
385
|
+
this.currentFrame!.dismissErrorBoundary?.();
|
|
386
|
+
this.requestReRender();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @returns boolean it looks like ... (passive is very shy) unless another render run in the meanwhile or a non-passive rerender request will dominate
|
|
391
|
+
*/
|
|
392
|
+
nextReRenderMightBePassive() {
|
|
393
|
+
return this.currentFrame?.recentRenderRun !== undefined && this.reRenderRequested === this.currentFrame.recentRenderRun;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
constructor(options: WatchedComponentOptions) {
|
|
398
|
+
this.options = options;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
*
|
|
403
|
+
* @param props
|
|
404
|
+
*/
|
|
405
|
+
applyNewProps(props: object) {
|
|
406
|
+
// Set / add new props:
|
|
407
|
+
for(const key in props) {
|
|
408
|
+
//@ts-ignore
|
|
409
|
+
this.watchedProps[key] = props[key];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Set non-existing to undefined:
|
|
413
|
+
for(const key in this.watchedProps) {
|
|
414
|
+
//@ts-ignore
|
|
415
|
+
this.watchedProps[key] = props[key];
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
*
|
|
422
|
+
* Lifecycle: Render + optional passive render + timespan until the next render (=because something new happened) or until the final unmount.
|
|
423
|
+
* Note: In case of an error and wrapped in a recoverable <ErrorBoundary>, the may not even be a mount but this Frame still exist.
|
|
424
|
+
*
|
|
425
|
+
*/
|
|
426
|
+
class Frame {
|
|
427
|
+
/**
|
|
428
|
+
* Result or result so far.
|
|
429
|
+
* - RenderRun, when component is currently rendering or beeing displayed (also for passive runs, if the passive run had that outcome)
|
|
430
|
+
* - undefined, when component was unmounted (and nothing was thrown / not loading)
|
|
431
|
+
* - Promise, when something is loading (with no fallback, etc) and therefore the component is in suspense (also for passive runs, if the passive run had that outcome)
|
|
432
|
+
* - Error when error was thrown during last render (also for passive runs, if the passive run had that outcome)
|
|
433
|
+
* - unknown: Something else was thrown during last render (also for passive runs, if the passive run had that outcome)
|
|
434
|
+
*/
|
|
435
|
+
result!: RenderRun | undefined | Promise<unknown> | Error | unknown;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The most recent render run
|
|
439
|
+
*/
|
|
440
|
+
recentRenderRun?: RenderRun;
|
|
441
|
+
|
|
442
|
+
persistent!: WatchedComponentPersistent;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* See {@link https://github.com/bvaughn/react-error-boundary?tab=readme-ov-file#dismiss-the-nearest-error-boundary}
|
|
446
|
+
* From optional package.
|
|
447
|
+
*/
|
|
448
|
+
dismissErrorBoundary?: () => void;
|
|
449
|
+
|
|
450
|
+
isListeningForChanges = false;
|
|
451
|
+
|
|
452
|
+
//watchedProxyFacade= new WatchedProxyFacade();
|
|
453
|
+
get watchedProxyFacade() {
|
|
454
|
+
// Use a global shared instance. Because there's no exclusive state inside the graph/handlers. And state.someObj = state.someObj does not cause us multiple nesting layers of proxies. Still this may not the final choice. When changing this mind also the `this.proxyHandler === other.proxyHandler` in RecordedPropertyRead#equals
|
|
455
|
+
return getWatchedProxyFacade();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
constructor() {
|
|
459
|
+
this.watchPropertyChange_changeListenerFn = this.watchPropertyChange_changeListenerFn.bind(this); // method is handed over as function but uses "this" inside.
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
startPropChangeListeningFns: (()=>void)[] = [];
|
|
463
|
+
/**
|
|
464
|
+
* Makes the frame become "alive". Listens for property changes and re-polls poll(...) statements.
|
|
465
|
+
* Calling it twice does not hurt.
|
|
466
|
+
*/
|
|
467
|
+
startListeningForChanges() {
|
|
468
|
+
if(this.isListeningForChanges) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.startPropChangeListeningFns.forEach(c => c());
|
|
473
|
+
|
|
474
|
+
this.persistent.loadRuns.forEach(lc => lc.activateRegularRePollingIfNeeded()); // Schedule re-polls
|
|
475
|
+
|
|
476
|
+
this.isListeningForChanges = true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
cleanUpPropChangeListenerFns: (()=>void)[] = [];
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @see startListeningForChanges
|
|
483
|
+
* @param deactivateRegularRePoll keep this true normally.
|
|
484
|
+
*/
|
|
485
|
+
stopListeningForChanges(deactivateRegularRePoll=true) {
|
|
486
|
+
if(!this.isListeningForChanges) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.cleanUpPropChangeListenerFns.forEach(c => c()); // Clean the listeners
|
|
491
|
+
if(deactivateRegularRePoll) {
|
|
492
|
+
this.persistent.loadRuns.forEach(lc => lc.deactivateRegularRePoll()); // Stop scheduled re-polls
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.isListeningForChanges = false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
handleWatchedPropertyChange() {
|
|
499
|
+
this.persistent.handleChangeEvent();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
watchPropertyChange(read: RecordedRead) {
|
|
503
|
+
//Diagnosis: Provoke errors early, cause the code at the bottom of this method looses the stacktrace to the user's jsx
|
|
504
|
+
if(this.persistent.options.watchOutside !== false) {
|
|
505
|
+
try {
|
|
506
|
+
if (read instanceof RecordedReadOnProxiedObject) {
|
|
507
|
+
installChangeTracker(read.obj);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
throw new Error(`Could not enhance the original object to track reads. This can fail, if it was created with some unsupported language constructs (defining read only properties; subclassing Array, Set or Map; ...). You can switch it off via the WatchedComponentOptions#watchOutside flag. I.e: const MyComponent = watchedComponent(props => {...}, {watchOutside: false})`, {cause: e});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Re-render on a change of the read value:
|
|
516
|
+
this.startPropChangeListeningFns.push(() => read.onAfterChange(this.watchPropertyChange_changeListenerFn /* Performance: We're not using an anonymous(=instance-changing) function here */, this.persistent.options.watchOutside !== false));
|
|
517
|
+
this.cleanUpPropChangeListenerFns.push(() => read.offAfterChange(this.watchPropertyChange_changeListenerFn /* Performance: We're not using an anonymous(=instance-changing) function here */));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
protected watchPropertyChange_changeListenerFn() {
|
|
521
|
+
if (currentRenderRun) {
|
|
522
|
+
throw new Error("You must not modify a watched object during the render run.");
|
|
523
|
+
}
|
|
524
|
+
this.handleWatchedPropertyChange();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Lifecycle: Starts when rendering and ends when unmounting or re-rendering the watchedComponent.
|
|
530
|
+
* - References to this can still exist when WatchedComponentPersistent is in a resumeable error state (is this a good idea? )
|
|
531
|
+
*/
|
|
532
|
+
class RenderRun {
|
|
533
|
+
frame!: Frame;
|
|
534
|
+
|
|
535
|
+
isPassive=false;
|
|
536
|
+
|
|
537
|
+
recordedReads: RecordedRead[] = [];
|
|
538
|
+
|
|
539
|
+
loadCallIdsSeen = new Set<LoadCall["id"]>();
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Increased, when we see a load(...) call
|
|
543
|
+
*/
|
|
544
|
+
loadCallIndex = 0;
|
|
545
|
+
|
|
546
|
+
onFinallyAfterUsersComponentFnListeners: (()=>void)[] = [];
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Cache of persistent.loadRuns.some(l => l.result.state === "pending")
|
|
550
|
+
*/
|
|
551
|
+
somePending?: Promise<unknown>;
|
|
552
|
+
somePendingAreCritical = false;
|
|
553
|
+
|
|
554
|
+
handleRenderFinishedSuccessfully() {
|
|
555
|
+
if(!this.isPassive) {
|
|
556
|
+
// Delete unused loadCalls
|
|
557
|
+
const keys = [...this.frame.persistent.loadCalls.keys()];
|
|
558
|
+
keys.forEach(key => {
|
|
559
|
+
if (!this.loadCallIdsSeen.has(key)) {
|
|
560
|
+
this.frame.persistent.loadCalls.delete(key);
|
|
561
|
+
}
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
// Delete unused loadRuns:
|
|
565
|
+
this.frame.persistent.loadRuns = this.frame.persistent.loadRuns.slice(0, this.loadCallIndex+1);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Body of useEffect
|
|
571
|
+
*/
|
|
572
|
+
handleEffectSetup() {
|
|
573
|
+
this.frame.persistent.hadASuccessfullMount = true;
|
|
574
|
+
this.frame.startListeningForChanges();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Called by useEffect before the next render oder before unmount(for suspense, for error or forever)
|
|
579
|
+
*/
|
|
580
|
+
handleEffectCleanup() {
|
|
581
|
+
// Call listeners:
|
|
582
|
+
this.frame.persistent.onceOnEffectCleanupListeners.forEach(fn => fn());
|
|
583
|
+
this.frame.persistent.onceOnEffectCleanupListeners = [];
|
|
584
|
+
|
|
585
|
+
let currentFrame = this.frame.persistent.currentFrame!;
|
|
586
|
+
if(currentFrame.result instanceof Error && currentFrame.dismissErrorBoundary !== undefined) { // Error is displayed ?
|
|
587
|
+
// Still listen for property changes to be able to recover from errors and clean up later:
|
|
588
|
+
if(this.frame !== currentFrame) { // this.frame is old ?
|
|
589
|
+
this.frame.stopListeningForChanges(false); // This frame's listeners can be cleaned now but still keep the polling alive (there's a conflict with double responsibility here / hacky solution)
|
|
590
|
+
}
|
|
591
|
+
this.frame.persistent.onceOnReRenderListeners.push(() => {
|
|
592
|
+
this.frame.stopListeningForChanges();
|
|
593
|
+
this.frame.persistent.loadRuns.forEach(lc => lc.deactivateRegularRePoll()); // hacky solution2: The lines above have propably skipped this, so do it now
|
|
594
|
+
}); //Instead clean up listeners next time
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
this.frame.stopListeningForChanges(); // Clean up now
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
let currentRenderRun: RenderRun| undefined;
|
|
602
|
+
|
|
603
|
+
export function watchedComponent<PROPS extends object>(componentFn:(props: PROPS) => any, options: WatchedComponentOptions = {}) {
|
|
604
|
+
const outerResult = (props: PROPS) => {
|
|
605
|
+
const [renderCounter, setRenderCounter] = useState(0);
|
|
606
|
+
const [persistent] = useState(new WatchedComponentPersistent(options));
|
|
607
|
+
persistent._doReRender = () => setRenderCounter(renderCounter+1);
|
|
608
|
+
|
|
609
|
+
const isPassive = persistent.nextReRenderMightBePassive()
|
|
610
|
+
|
|
611
|
+
// Apply the new props (may trigger change listeners and therefore requestReRender() )
|
|
612
|
+
persistent.reRenderRequested = true; // this prevents new re-renders
|
|
613
|
+
persistent.requestReRender(); // Test, that this does not cause an infinite loop. (line can be removed when running stable)
|
|
614
|
+
persistent.applyNewProps(props);
|
|
615
|
+
|
|
616
|
+
persistent.reRenderRequested = false;
|
|
617
|
+
|
|
618
|
+
// Call remaining listeners, because may be the render was not "requested" through code in this package but happened some other way:
|
|
619
|
+
persistent.onceOnReRenderListeners.forEach(fn => fn());
|
|
620
|
+
persistent.onceOnReRenderListeners = [];
|
|
621
|
+
|
|
622
|
+
// Create frame:
|
|
623
|
+
let frame = isPassive && persistent.currentFrame !== undefined ? persistent.currentFrame : new Frame();
|
|
624
|
+
persistent.currentFrame = frame;
|
|
625
|
+
frame.persistent = persistent;
|
|
626
|
+
|
|
627
|
+
// Create RenderRun:
|
|
628
|
+
currentRenderRun === undefined || throwError("Illegal state: already in currentRenderRun");
|
|
629
|
+
const renderRun = currentRenderRun = new RenderRun();
|
|
630
|
+
renderRun.frame = frame;
|
|
631
|
+
renderRun.isPassive = isPassive;
|
|
632
|
+
frame.recentRenderRun = currentRenderRun;
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
// Register dismissErrorBoundary function:
|
|
636
|
+
if(typeof useErrorBoundary === "function") { // Optional package was loaded?
|
|
637
|
+
if(useContext(ErrorBoundaryContext)) { // Inside an error boundary?
|
|
638
|
+
frame.dismissErrorBoundary = useErrorBoundary().resetBoundary;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
renderRun.handleEffectSetup();
|
|
644
|
+
return () => renderRun.handleEffectCleanup();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
// Install read listener:
|
|
650
|
+
let readListener = (read: RecordedRead) => {
|
|
651
|
+
if(!renderRun.isPassive) { // Active run ?
|
|
652
|
+
frame.watchPropertyChange(read);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
renderRun.recordedReads.push(read);
|
|
656
|
+
};
|
|
657
|
+
frame.watchedProxyFacade.onAfterRead(readListener)
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
try {
|
|
661
|
+
let result = componentFn(persistent.watchedProps as PROPS); // Run the user's component function
|
|
662
|
+
renderRun.handleRenderFinishedSuccessfully();
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
renderRun.onFinallyAfterUsersComponentFnListeners.forEach(l => l()); // Call listeners
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
if(persistent.nextReRenderMightBePassive()) {
|
|
671
|
+
return createElement(Fragment, null); // Don't go to suspense **now**. The passive render might have a different outcome. (rerender will be done, see "finally")
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
frame.result = e;
|
|
675
|
+
if(e instanceof Promise) {
|
|
676
|
+
if(!persistent.hadASuccessfullMount) {
|
|
677
|
+
// Handle the suspense ourself. Cause the react Suspense does not restore the state by useState :(
|
|
678
|
+
return createElement(Fragment, null); // Return an empty element (might cause a short screen flicker) and render again.
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if(options.fallback) {
|
|
682
|
+
return options.fallback;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// React's <Suspense> seems to keep this component mounted (hidden), so here's no need for an artificial renderRun.startListeningForChanges();
|
|
686
|
+
}
|
|
687
|
+
else { // Error?
|
|
688
|
+
if(frame.dismissErrorBoundary !== undefined) { // inside (recoverable) error boundary ?
|
|
689
|
+
// The useEffects won't fire, so whe simulate the frame's effect lifecycle here:
|
|
690
|
+
frame.startListeningForChanges();
|
|
691
|
+
persistent.onceOnReRenderListeners.push(() => {frame.stopListeningForChanges()});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
throw e;
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
frame.watchedProxyFacade.offAfterRead(readListener);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
renderRun.recordedReads = []; // renderRun is still referenced in closures, but this field is not needed, so let's not hold a big grown array here and may be prevent memory leaks
|
|
702
|
+
currentRenderRun = undefined;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (options.memo === false) {
|
|
707
|
+
return outerResult;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return memo(outerResult);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
type WatchedOptions = {
|
|
714
|
+
/**
|
|
715
|
+
* TODO: Implement
|
|
716
|
+
* Called, when a deep property was changed through the proxy.
|
|
717
|
+
*/
|
|
718
|
+
onChange?: () => void
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* TODO: Implement
|
|
722
|
+
* Called on a change to one of those properties, that were read-recorded in the component function (through the proxy of course).
|
|
723
|
+
* Reacts also on external changes / not done through the proxy.
|
|
724
|
+
*/
|
|
725
|
+
onRecordedChange?: () => void
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function watched<T extends object>(obj: T, options?: WatchedOptions): T {
|
|
729
|
+
currentRenderRun || throwError("watched is not used from inside a watchedComponent");
|
|
730
|
+
return currentRenderRun!.frame.watchedProxyFacade.getProxyFor(obj);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function useWatchedState(initial: object, options?: WatchedOptions) {
|
|
734
|
+
currentRenderRun || throwError("useWatchedState is not used from inside a watchedComponent");
|
|
735
|
+
|
|
736
|
+
const [state] = useState(initial);
|
|
737
|
+
return watched(state);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Records the values, that are **immediately** accessed in the loader function. Treats them as dependencies and re-executes the loader when any of these change.
|
|
742
|
+
* <p>
|
|
743
|
+
* Opposed to {@link load}, it does not treat all previously accessed properties as dependencies
|
|
744
|
+
* </p>
|
|
745
|
+
* <p>
|
|
746
|
+
* Immediately means: Before the promise is returned. I.e. does not record any more after your fetch finished.
|
|
747
|
+
* </p>
|
|
748
|
+
* @param loader
|
|
749
|
+
*/
|
|
750
|
+
function useLoad<T>(loader: () => Promise<T>): T {
|
|
751
|
+
return undefined as T;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
type LoadOptions = {
|
|
755
|
+
/**
|
|
756
|
+
* For {@link LoadOptions#preserve preserving} the result's object identity.
|
|
757
|
+
* Normally, this is obtained from the call stack information plus the {@link LoadOptions#key}.
|
|
758
|
+
*
|
|
759
|
+
* @see LoadOptions#key
|
|
760
|
+
*/
|
|
761
|
+
id?: string | number | object
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Helps identifying the load(...) call from inside a loop for {@link LoadOptions#preserve preserving} the result's object identity.
|
|
765
|
+
* @see LoadOptions#id
|
|
766
|
+
*/
|
|
767
|
+
key?: string | number
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* If you specify a fallback, the component can be immediately rendered during loading.
|
|
771
|
+
* <p>
|
|
772
|
+
* undefined = undefined as fallback.
|
|
773
|
+
* </p>
|
|
774
|
+
*/
|
|
775
|
+
fallback?: unknown
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Performance: Will return the old value from a previous load, while this is still loading. This causes less disturbance (i.e. triggering dependent loads) while switching back to the fallback and then to a real value again.
|
|
779
|
+
* <p>Best used in combination with {@link isLoading} and {@link fallback}</p>
|
|
780
|
+
* <p>Default: false</p>
|
|
781
|
+
*/
|
|
782
|
+
silent?: boolean
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Performance: Set to false, to mark following `load(...)` statements do not depend on the result. I.e when used only for immediate rendering or passed to child components only. I.e. <div>{load(...)}/div> or `<MySubComponent param={load(...)} />`:
|
|
786
|
+
* Therefore, the following `load(...)` statements may not need a reload and can run in parallel.
|
|
787
|
+
* <p>
|
|
788
|
+
* Default: true
|
|
789
|
+
* </p>
|
|
790
|
+
*/
|
|
791
|
+
critical?: boolean
|
|
792
|
+
|
|
793
|
+
// Seems not possible because loaderFn is mostly an anonymous function and cannot be re-identified
|
|
794
|
+
// /**
|
|
795
|
+
// * Values which the loaderFn depends on. If any of these change, it will do a reload.
|
|
796
|
+
// * <p>
|
|
797
|
+
// * By default it will do a very safe and comfortable in-doubt-do-a-reload, meaning: It depends on the props + all `usedWatchedState(...)` + all `watched(...)` + the result of previous `load(...)` statements.
|
|
798
|
+
// * </p>
|
|
799
|
+
// */
|
|
800
|
+
// deps?: unknown[]
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Poll after this amount of milliseconds
|
|
804
|
+
*/
|
|
805
|
+
poll?: number
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* {@link isLoading} Can filter for only the load(...) statements with this given name.
|
|
809
|
+
*/
|
|
810
|
+
name?: string
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
*
|
|
814
|
+
* <p>Default: true</p>
|
|
815
|
+
*/
|
|
816
|
+
preserve?: boolean | PreserveOptions
|
|
817
|
+
}
|
|
818
|
+
type PollOptions = {
|
|
819
|
+
/**
|
|
820
|
+
* Interval in milliseconds
|
|
821
|
+
*/
|
|
822
|
+
interval: number
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* - true = interval means loaderFn-start to loaderFn-start
|
|
826
|
+
* - false = interval means loaderFn-end to loaderFn-start (the longer loaderFn takes, the more time till next re-poll)
|
|
827
|
+
* <p>
|
|
828
|
+
* Default: true
|
|
829
|
+
* </p>
|
|
830
|
+
*/
|
|
831
|
+
fixedInterval?:boolean
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Runs the async loaderFn and re-renders, if its promise was resolved. Also re-renders and re-runs loaderFn, when some of its watched dependencies, used prior or instantly in the loaderFn, change.
|
|
836
|
+
* Puts the component into suspense while loading. Throws an error when loaderFn throws an error or its promise is rejected. Resumes from react-error-boundary automatically when loaderFn was re-run(because of the above).
|
|
837
|
+
* <p>
|
|
838
|
+
* {@link https://github.com/bogeeee/react-deepwatch#and-less-loading-code Usage}.
|
|
839
|
+
* </p>
|
|
840
|
+
* @param loaderFn
|
|
841
|
+
* @param options
|
|
842
|
+
*/
|
|
843
|
+
export function load<T,FALLBACK>(loaderFn: (oldResult?: T) => Promise<T>, options?: Omit<LoadOptions, "fallback">): T
|
|
844
|
+
/**
|
|
845
|
+
* Runs the async loaderFn and re-renders, if its promise was resolved. Also re-renders and re-runs loaderFn, when some of its watched dependencies, used prior or instantly in the loaderFn, change.
|
|
846
|
+
* Puts the component into suspense while loading. Throws an error when loaderFn throws an error or its promise is rejected. Resumes from react-error-boundary automatically when loaderFn was re-run(because of the above).
|
|
847
|
+
* <p>
|
|
848
|
+
* {@link https://github.com/bogeeee/react-deepwatch#and-less-loading-code Usage}.
|
|
849
|
+
* </p>
|
|
850
|
+
* @param loaderFn
|
|
851
|
+
* @param options
|
|
852
|
+
*/
|
|
853
|
+
export function load<T,FALLBACK>(loaderFn: (oldResult?: T) => Promise<T>, options: LoadOptions & {fallback: FALLBACK}): T | FALLBACK
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Runs the async loaderFn and re-renders, if its promise was resolved. Also re-renders and re-runs loaderFn, when some of its watched dependencies, used prior or instantly in the loaderFn, change.
|
|
857
|
+
* Puts the component into suspense while loading. Throws an error when loaderFn throws an error or its promise is rejected. Resumes from react-error-boundary automatically when loaderFn was re-run(because of the above).
|
|
858
|
+
* <p>
|
|
859
|
+
* {@link https://github.com/bogeeee/react-deepwatch#and-less-loading-code Usage}.
|
|
860
|
+
* </p>
|
|
861
|
+
* @param loaderFn
|
|
862
|
+
* @param options
|
|
863
|
+
*/
|
|
864
|
+
export function load(loaderFn: (oldResult?: unknown) => Promise<unknown>, options: LoadOptions & Partial<PollOptions> = {}): any {
|
|
865
|
+
const callStack = new Error("load(...) was called") // Look not here, but one level down in the stack, where you called load(...)
|
|
866
|
+
|
|
867
|
+
// Wording:
|
|
868
|
+
// - "previous" means: load(...) statements more upwards in the user's code
|
|
869
|
+
// - "last" means: this load call but from a past frame
|
|
870
|
+
|
|
871
|
+
// Validity checks:
|
|
872
|
+
typeof loaderFn === "function" || throwError("loaderFn is not a function");
|
|
873
|
+
if(currentRenderRun === undefined) throw new Error("load is not used from inside a watchedComponent")
|
|
874
|
+
|
|
875
|
+
const renderRun = currentRenderRun;
|
|
876
|
+
const frame = renderRun.frame
|
|
877
|
+
const persistent = frame.persistent;
|
|
878
|
+
const recordedReadsSincePreviousLoadCall = renderRun.recordedReads; renderRun.recordedReads = []; // Pop recordedReads
|
|
879
|
+
const callerSourceLocation = callStack.stack ? getCallerSourceLocation(callStack.stack) : undefined;
|
|
880
|
+
|
|
881
|
+
// Determine loadCallId:
|
|
882
|
+
let loadCallId: LoadCall["id"] | undefined
|
|
883
|
+
if(options.id !== undefined) {
|
|
884
|
+
options.key === undefined || throwError("Must not set both: LoadOptions#id and LoadOptions#key"); // Validity check
|
|
885
|
+
|
|
886
|
+
loadCallId = options.id;
|
|
887
|
+
|
|
888
|
+
!renderRun.loadCallIdsSeen.has(loadCallId) || throwError(`LoadOptions#id=${loadCallId} is not unique`);
|
|
889
|
+
} else if (options.key !== undefined) {
|
|
890
|
+
callerSourceLocation || throwError("No callstack available to compose the id. Please specify LoadOptions#id instead of LoadOptions#key"); // validity check
|
|
891
|
+
loadCallId = `${callerSourceLocation}___${options.key}` // I.e. ...
|
|
892
|
+
!renderRun.loadCallIdsSeen.has(loadCallId) || throwError(`LoadOptions#key=${options.key} is used multiple times / is not unique here.`);
|
|
893
|
+
} else {
|
|
894
|
+
loadCallId = callerSourceLocation; // from source location only
|
|
895
|
+
}
|
|
896
|
+
const isUnique = !(renderRun.loadCallIdsSeen.has(loadCallId) || persistent.loadCalls.get(loadCallId) === null);
|
|
897
|
+
renderRun.loadCallIdsSeen.add(loadCallId);
|
|
898
|
+
|
|
899
|
+
// Find the loadCall or create it:
|
|
900
|
+
let loadCall: LoadCall | undefined
|
|
901
|
+
if(isUnique) {
|
|
902
|
+
loadCall = persistent.loadCalls.get(loadCallId) as (LoadCall | undefined);
|
|
903
|
+
}
|
|
904
|
+
if(loadCall === undefined) {
|
|
905
|
+
loadCall = new LoadCall(loadCallId, persistent, options, callStack, callerSourceLocation);
|
|
906
|
+
}
|
|
907
|
+
persistent.loadCalls.set(loadCallId, isUnique?loadCall:null);
|
|
908
|
+
|
|
909
|
+
loadCall.options = options; // Update options. It is allowed that these can change over time. I.e. the poll interval or the name.
|
|
910
|
+
|
|
911
|
+
// Determine lastLoadRun:
|
|
912
|
+
let lastLoadRun = renderRun.loadCallIndex < persistent.loadRuns.length?persistent.loadRuns[renderRun.loadCallIndex]:undefined;
|
|
913
|
+
if(lastLoadRun) {
|
|
914
|
+
lastLoadRun.loaderFn = options.interval ? loaderFn : undefined; // Update. only needed, when polling.
|
|
915
|
+
lastLoadRun.loadCall.id === loadCall.id || throwError(new Error("Illegal state: lastLoadRun associated with different LoadCall. Please make sure that you don't use non-`watched(...)` inputs (useState, useContext) in your watchedComponent. " + `. Debug info: Ids: ${lastLoadRun.loadCall.id} vs. ${loadCall.id}. See cause for falsely associcated loadCall.`, {cause: lastLoadRun.loadCall.diagnosis_callstack})); // Validity check
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const fallback = loadCall.getFallbackValue();
|
|
919
|
+
|
|
920
|
+
try {
|
|
921
|
+
if(renderRun.isPassive) {
|
|
922
|
+
// Don't look at recorded reads. Assume the order has not changed
|
|
923
|
+
|
|
924
|
+
// Validity check:
|
|
925
|
+
if(lastLoadRun === undefined) {
|
|
926
|
+
//throw new Error("More load(...) statements in render run for status indication seen than last time. isLoading()'s result must not influence the structure/order of load(...) statements.");
|
|
927
|
+
// you can still get here when there was a some critical pending load before this, that had sliced off the rest. TODO: don't slice and just mark them as invalid
|
|
928
|
+
|
|
929
|
+
if(fallback) {
|
|
930
|
+
return fallback.value;
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
throw new Error(`When using isLoading(), you must specify fallbacks for all your load statements: load(..., {fallback: some-fallback-value})`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
//** return lastLoadRun.result:
|
|
938
|
+
if(lastLoadRun.result.state === "resolved") {
|
|
939
|
+
return options.critical !== false?watched(lastLoadRun.result.resolvedValue):lastLoadRun.result.resolvedValue;
|
|
940
|
+
}
|
|
941
|
+
else if(lastLoadRun?.result.state === "rejected") {
|
|
942
|
+
throw lastLoadRun.result.rejectReason;
|
|
943
|
+
}
|
|
944
|
+
else if(lastLoadRun.result.state === "pending") {
|
|
945
|
+
if(fallback) {
|
|
946
|
+
return fallback.value;
|
|
947
|
+
}
|
|
948
|
+
throw lastLoadRun.result.promise;
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
throw new Error("Unhandled state");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
let result = inner();
|
|
956
|
+
if(options.critical === false) {
|
|
957
|
+
return result; // non-watched and add no dependency
|
|
958
|
+
}
|
|
959
|
+
renderRun.recordedReads.push(new RecordedValueRead(result)); // Add as dependency for the next loads
|
|
960
|
+
return watched(result);
|
|
961
|
+
}
|
|
962
|
+
finally {
|
|
963
|
+
renderRun.loadCallIndex++;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
function inner() {
|
|
969
|
+
const recordedReadsAreEqualSinceLastCall = lastLoadRun && recordedReadsArraysAreEqual(recordedReadsSincePreviousLoadCall, lastLoadRun.recordedReadsBefore)
|
|
970
|
+
if(!recordedReadsAreEqualSinceLastCall) {
|
|
971
|
+
persistent.loadRuns = persistent.loadRuns.slice(0, renderRun.loadCallIndex); // Erase all loadRuns after here (including this one).
|
|
972
|
+
lastLoadRun = undefined;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Can we use the result from last call ?
|
|
977
|
+
*/
|
|
978
|
+
const canReuseLastResult = () => {
|
|
979
|
+
if(!lastLoadRun) { // call was not recorded last render or is invalid?
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
if (!recordedReadsAreEqualSinceLastCall) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (lastLoadRun.recordedReadsInsideLoaderFn.some((r => r.isChanged))) { // I.e for "load( () => { fetch(props.x, myLocalValue) }) )" -> props.x or myLocalValue has changed?
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (lastLoadRun.result.state === "resolved") {
|
|
991
|
+
return {result: lastLoadRun.result.resolvedValue}
|
|
992
|
+
}
|
|
993
|
+
if (lastLoadRun.result.state === "pending") {
|
|
994
|
+
renderRun.somePending = lastLoadRun.result.promise;
|
|
995
|
+
renderRun.somePendingAreCritical ||= (options.critical !== false);
|
|
996
|
+
if (fallback) { // Fallback available ?
|
|
997
|
+
return {result: fallback.value};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
lastLoadRun.recordedReadsInsideLoaderFn.forEach(read => frame.watchPropertyChange(read)) // Also watch recordedReadsInsideLoaderFn (again in this frame)
|
|
1001
|
+
throw lastLoadRun.result.promise; // Throwing a promise will put the react component into suspense state
|
|
1002
|
+
} else if (lastLoadRun.result.state === "rejected") {
|
|
1003
|
+
lastLoadRun.recordedReadsInsideLoaderFn.forEach(read => frame.watchPropertyChange(read)) // Also watch recordedReadsInsideLoaderFn (again in this frame)
|
|
1004
|
+
throw lastLoadRun.result.rejectReason;
|
|
1005
|
+
} else {
|
|
1006
|
+
throw new Error("Invalid state of lastLoadRun.result.state")
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const canReuse = canReuseLastResult();
|
|
1011
|
+
if (canReuse !== false) { // can re-use ?
|
|
1012
|
+
const lastCall = persistent.loadRuns[renderRun.loadCallIndex];
|
|
1013
|
+
|
|
1014
|
+
lastCall.recordedReadsInsideLoaderFn.forEach(read => frame.watchPropertyChange(read)) // Also watch recordedReadsInsideLoaderFn
|
|
1015
|
+
|
|
1016
|
+
return canReuse.result; // return proxy'ed result from last call:
|
|
1017
|
+
}
|
|
1018
|
+
else { // cannot use last result ?
|
|
1019
|
+
if(renderRun.somePending && renderRun.somePendingAreCritical) { // Performance: Some previous (and dependent) results are pending, so loading this one would trigger a reload soon
|
|
1020
|
+
// don't make a new call
|
|
1021
|
+
if(fallback) {
|
|
1022
|
+
return fallback.value;
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
throw renderRun.somePending;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// *** make a loadRun / exec loaderFn ***:
|
|
1030
|
+
|
|
1031
|
+
let loadRun = new LoadRun(loadCall!, loaderFn, renderRun.loadCallIndex);
|
|
1032
|
+
loadRun.recordedReadsBefore = recordedReadsSincePreviousLoadCall;
|
|
1033
|
+
const resultPromise = Promise.resolve(loadRun.exec()); // Exec loaderFn
|
|
1034
|
+
loadRun.loaderFn = options.interval?loadRun.loaderFn:undefined; // Remove reference if not needed to not risk leaking memory
|
|
1035
|
+
loadRun.recordedReadsInsideLoaderFn = renderRun.recordedReads; renderRun.recordedReads = []; // pop and remember the (immediate) reads from inside the loaderFn
|
|
1036
|
+
|
|
1037
|
+
resultPromise.then((value) => {
|
|
1038
|
+
loadRun.result = {state: "resolved", resolvedValue: value};
|
|
1039
|
+
|
|
1040
|
+
if(loadRun.isObsolete) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/*
|
|
1045
|
+
const otherLoadsAreWaiting=true;// Other loads are waiting for this critical loadRun? TODO
|
|
1046
|
+
const wasErrored = true; // TODO
|
|
1047
|
+
if (fallback && (fallback.value === value) && !otherLoadsAreWaiting && !currentRenderRun!.isPassive && !wasErrored) { // Result is same as fallback (already displayed) + this situation allows to skip re-rendering because it would stay unchanged?
|
|
1048
|
+
// Not worth it / too risky, just to save one rerender. Maybe i have overseen something.
|
|
1049
|
+
if(persistent.currentFrame?.isListeningForChanges) { // Frame is "alive" ?
|
|
1050
|
+
loadRun.activateRegularRePollingIfNeeded();
|
|
1051
|
+
}
|
|
1052
|
+
} else {
|
|
1053
|
+
persistent.handleChangeEvent(); // Will also do a rerender and call activateRegularRePollingIfNeeded, like above
|
|
1054
|
+
}
|
|
1055
|
+
*/
|
|
1056
|
+
|
|
1057
|
+
persistent.handleChangeEvent();
|
|
1058
|
+
});
|
|
1059
|
+
resultPromise.catch(reason => {
|
|
1060
|
+
loadRun.result = {state: "rejected", rejectReason: reason}
|
|
1061
|
+
|
|
1062
|
+
if(loadRun.isObsolete) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
persistent.handleChangeEvent(); // Re-render. The next render will see state=rejected for this load statement and throw it then.
|
|
1067
|
+
})
|
|
1068
|
+
loadRun.result = {state: "pending", promise: resultPromise};
|
|
1069
|
+
|
|
1070
|
+
persistent.loadRuns[renderRun.loadCallIndex] = loadRun; // add / replace
|
|
1071
|
+
|
|
1072
|
+
renderRun.somePending = resultPromise;
|
|
1073
|
+
renderRun.somePendingAreCritical ||= (options.critical !== false);
|
|
1074
|
+
|
|
1075
|
+
if (fallback) {
|
|
1076
|
+
return fallback.value;
|
|
1077
|
+
} else {
|
|
1078
|
+
throw resultPromise; // Throwing a promise will put the react component into suspense state
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function watched(value: unknown) { return (value !== null && typeof value === "object")?frame.watchedProxyFacade.getProxyFor(value):value }
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
*
|
|
1087
|
+
* @param callStack
|
|
1088
|
+
* @returns i.e. "at http://localhost:5173/index.tsx:98:399"
|
|
1089
|
+
*/
|
|
1090
|
+
function getCallerSourceLocation(callStack: String | undefined) {
|
|
1091
|
+
callStack = callStack!.replace(/.*load\(\.\.\.\) was called\s*/, ""); // Remove trailing error from callstack
|
|
1092
|
+
const callStackRows = callStack! .split("\n");
|
|
1093
|
+
callStackRows.length >= 2 || throwError(`Unexpected callstack format: ${callStack}`); // Validity check
|
|
1094
|
+
let result = callStackRows[1].trim();
|
|
1095
|
+
result !== "" || throwError("Illegal result");
|
|
1096
|
+
result = result.replace(/at\s*/, ""); // Remove trailing "at "
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Probe if a <code>load(...)</code> statement directly inside this watchedComponent is currently loading.
|
|
1103
|
+
* Note: It's mostly needed to also specify a {@link LoadOptions#fallback} in the load statement's options to produce a valid render result while loading. Otherwise the whole component goes into suspense.
|
|
1104
|
+
* <p>
|
|
1105
|
+
* Example. This uses isLoading() to determine if the Dropdown list should be faded/transparent during while items are loading:
|
|
1106
|
+
* <pre><code>
|
|
1107
|
+
* return <select style={{opacity: isLoading("dropdownItems")?0.5:1}}>
|
|
1108
|
+
* {load(() => fetchMyDropdownItems(), {name: "dropdownItems", fallback: ["loading items"]}).map(i => <option value="{i}">{i}</option>)}
|
|
1109
|
+
* </select>
|
|
1110
|
+
* </code></pre>
|
|
1111
|
+
* </p>
|
|
1112
|
+
* <p>
|
|
1113
|
+
* Caveat: You must not use this for a condition that cuts away a load(...) statement in the middle of your render code. This is because an extra render run is issued for isLoading() and the load(...) statements are re-matched by their order.
|
|
1114
|
+
* </p>
|
|
1115
|
+
* @param nameFilter When set, consider only those with the given {@link LoadOptions#name}. I.e. <code>load(..., {name: "myDropdownListEntries"})</code>
|
|
1116
|
+
*
|
|
1117
|
+
*/
|
|
1118
|
+
export function isLoading(nameFilter?: string): boolean {
|
|
1119
|
+
const renderRun = currentRenderRun;
|
|
1120
|
+
// Validity check:
|
|
1121
|
+
if(renderRun === undefined) throw new Error("isLoading is not used from inside a watchedComponent")
|
|
1122
|
+
|
|
1123
|
+
return probe(() => renderRun.frame.persistent.loadRuns.some(c => c.result.state === "pending" && (!nameFilter || c.name === nameFilter)), false);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Probe if a <code>load(...)</code> statement directly inside this watchedComponent failed.
|
|
1128
|
+
* <p>
|
|
1129
|
+
* Example:
|
|
1130
|
+
* <pre><code>
|
|
1131
|
+
* if(loadFailed()) {
|
|
1132
|
+
* return <div>Load failed: {loadFailed().message}</div>;
|
|
1133
|
+
* }
|
|
1134
|
+
*
|
|
1135
|
+
* return <div>My component content {load(...)} </div>
|
|
1136
|
+
* </code></pre>
|
|
1137
|
+
* </p>
|
|
1138
|
+
* <p>
|
|
1139
|
+
* Caveat: You must not use this for a condition that cuts away a load(...) statement in the middle of your render code. This is because an extra render run is issued for loadFailed() and the load(...) statements are re-matched by their order.
|
|
1140
|
+
* </p>
|
|
1141
|
+
* @param nameFilter When set, consider only those with the given {@link LoadOptions#name}. I.e. <code>load(..., {name: "myDropdownListEntries"})</code>
|
|
1142
|
+
* @returns unknown The thrown value of the loaderFn or undefined if everything is ok.
|
|
1143
|
+
*/
|
|
1144
|
+
export function loadFailed(nameFilter?: string): unknown {
|
|
1145
|
+
const renderRun = currentRenderRun;
|
|
1146
|
+
// Validity check:
|
|
1147
|
+
if(renderRun === undefined) throw new Error("isLoading is not used from inside a watchedComponent")
|
|
1148
|
+
|
|
1149
|
+
return probe(() => {
|
|
1150
|
+
return (renderRun.frame.persistent.loadRuns.find(c => c.result.state === "rejected" && (!nameFilter || c.name === nameFilter))?.result as any)?.rejectReason;
|
|
1151
|
+
}, undefined);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Like {@link load}, but re-runs loaderFn regularly at the interval, specified in the options.
|
|
1156
|
+
* <p>
|
|
1157
|
+
* Example: <code>return <div>The current outside temperature is { poll( async () => await fetchTemperatureFromServer(), {interval: 1000} ) }° </div></code> *
|
|
1158
|
+
* </p>
|
|
1159
|
+
* <p>
|
|
1160
|
+
* Polling is still continued in recoverable error cases, when
|
|
1161
|
+
* </p>
|
|
1162
|
+
* - loaderFn fails but your watchedComponent catches it and returns fine.
|
|
1163
|
+
* - Your watchedComponent returns with an error(because of this loaderFn or some other reason) and it is wrapped in a react-error-boundary.
|
|
1164
|
+
*
|
|
1165
|
+
* <p>
|
|
1166
|
+
* Note, that after the initial load, re-polling is done <strong>very silently</strong>. Meaning, there's no suspense / fallback / isLoading indicator involved.
|
|
1167
|
+
* </p>
|
|
1168
|
+
* @param loaderFn
|
|
1169
|
+
* @param options
|
|
1170
|
+
*/
|
|
1171
|
+
export function poll<T,FALLBACK>(loaderFn: (oldResult?: T) => Promise<T>, options: Omit<LoadOptions, "fallback"> & PollOptions): T
|
|
1172
|
+
/**
|
|
1173
|
+
* Like {@link load}, but re-runs loaderFn regularly at the interval, specified in the options.
|
|
1174
|
+
* <p>
|
|
1175
|
+
* Example: <code>return <div>The current outside temperature is { async poll( await () => fetchTemperatureFromServer(), {interval: 1000} ) }° </div></code> *
|
|
1176
|
+
* </p>
|
|
1177
|
+
* <p>
|
|
1178
|
+
* Polling is still continued in recoverable error cases, when
|
|
1179
|
+
* </p>
|
|
1180
|
+
* - loaderFn fails but your watchedComponent catches it and returns fine.
|
|
1181
|
+
* - Your watchedComponent returns with an error(because of this loaderFn or some other reason) and it is wrapped in a react-error-boundary.
|
|
1182
|
+
*
|
|
1183
|
+
* <p>
|
|
1184
|
+
* Note, that after the initial load, re-polling is done <strong>very silently</strong>. Meaning, there's no suspense / fallback / isLoading indicator involved.
|
|
1185
|
+
* </p>
|
|
1186
|
+
* @param loaderFn
|
|
1187
|
+
* @param options
|
|
1188
|
+
*/
|
|
1189
|
+
export function poll<T,FALLBACK>(loaderFn: (oldResult?: T) => Promise<T>, options: LoadOptions & {fallback: FALLBACK} & PollOptions): T | FALLBACK
|
|
1190
|
+
/**
|
|
1191
|
+
* Like {@link load}, but re-runs loaderFn regularly at the interval, specified in the options.
|
|
1192
|
+
* <p>
|
|
1193
|
+
* Example: <code>return <div>The current outside temperature is { poll( async () => await fetchTemperatureFromServer(), {interval: 1000} ) }° </div></code> *
|
|
1194
|
+
* </p>
|
|
1195
|
+
* <p>
|
|
1196
|
+
* Polling is still continued in recoverable error cases, when
|
|
1197
|
+
* </p>
|
|
1198
|
+
* - loaderFn fails but your watchedComponent catches it and returns fine.
|
|
1199
|
+
* - Your watchedComponent returns with an error(because of this loaderFn or some other reason) and it is wrapped in a react-error-boundary.
|
|
1200
|
+
*
|
|
1201
|
+
* <p>
|
|
1202
|
+
* Note, that after the initial load, re-polling is done <strong>very silently</strong>. Meaning, there's no suspense / fallback / isLoading indicator involved.
|
|
1203
|
+
* </p>
|
|
1204
|
+
* @param loaderFn
|
|
1205
|
+
* @param options
|
|
1206
|
+
*/
|
|
1207
|
+
export function poll(loaderFn: (oldResult?: unknown) => Promise<unknown>, options: LoadOptions & PollOptions): any {
|
|
1208
|
+
return load(loaderFn, options);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* For isLoading and isError. Makes a passive render run if these a
|
|
1213
|
+
* @param probeFn
|
|
1214
|
+
* @param defaultResult
|
|
1215
|
+
*/
|
|
1216
|
+
function probe<T>(probeFn: () => T, defaultResult: T) {
|
|
1217
|
+
const renderRun = currentRenderRun;
|
|
1218
|
+
// Validity check:
|
|
1219
|
+
if(renderRun === undefined) throw new Error("Not used from inside a watchedComponent")
|
|
1220
|
+
|
|
1221
|
+
if(renderRun.isPassive) {
|
|
1222
|
+
return probeFn();
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
renderRun.onFinallyAfterUsersComponentFnListeners.push(() => {
|
|
1226
|
+
if(probeFn() !== defaultResult) {
|
|
1227
|
+
renderRun.frame.persistent.requestReRender(currentRenderRun) // Request passive render.
|
|
1228
|
+
}
|
|
1229
|
+
})
|
|
1230
|
+
|
|
1231
|
+
return defaultResult;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
export function debug_tagComponent(name: string) {
|
|
1236
|
+
currentRenderRun!.frame.persistent.debug_tag = name;
|
|
1237
|
+
}
|