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/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
+ }