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.js
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.watchedComponent = watchedComponent;
|
|
4
|
+
exports.useWatchedState = useWatchedState;
|
|
5
|
+
exports.load = load;
|
|
6
|
+
exports.isLoading = isLoading;
|
|
7
|
+
exports.loadFailed = loadFailed;
|
|
8
|
+
exports.poll = poll;
|
|
9
|
+
exports.debug_tagComponent = debug_tagComponent;
|
|
10
|
+
const proxy_facades_1 = require("proxy-facades");
|
|
11
|
+
const Util_1 = require("./Util");
|
|
12
|
+
const react_1 = require("react");
|
|
13
|
+
const react_error_boundary_1 = require("react-error-boundary");
|
|
14
|
+
const preserve_1 = require("./preserve");
|
|
15
|
+
let watchedProxyFacade;
|
|
16
|
+
function getWatchedProxyFacade() {
|
|
17
|
+
return watchedProxyFacade || (watchedProxyFacade = new proxy_facades_1.WatchedProxyFacade()); // Lazy initialize global variable
|
|
18
|
+
}
|
|
19
|
+
let debug_idGenerator = 0;
|
|
20
|
+
/**
|
|
21
|
+
* Contains the preconditions and the state / polling state for a load(...) statement.
|
|
22
|
+
* 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).
|
|
23
|
+
*/
|
|
24
|
+
class LoadRun {
|
|
25
|
+
get isObsolete() {
|
|
26
|
+
return !(this.loadCall.watchedComponentPersistent.loadRuns.length > this.cache_index && this.loadCall.watchedComponentPersistent.loadRuns[this.cache_index] === this); // this.watchedComponentPersistent.loadRuns does not contain this?
|
|
27
|
+
}
|
|
28
|
+
get options() {
|
|
29
|
+
return this.loadCall.options;
|
|
30
|
+
}
|
|
31
|
+
get name() {
|
|
32
|
+
return this.options.name;
|
|
33
|
+
}
|
|
34
|
+
async exec() {
|
|
35
|
+
try {
|
|
36
|
+
if (this.options.fixedInterval !== false)
|
|
37
|
+
this.lastExecTime = new Date(); // Take timestamp
|
|
38
|
+
const lastResult = this.loadCall.lastResult;
|
|
39
|
+
let result = await this.loaderFn(lastResult);
|
|
40
|
+
if (this.options.preserve !== false) { // Preserve enabled?
|
|
41
|
+
if ((0, Util_1.isObject)(result)) { // Result is mergeable ?
|
|
42
|
+
this.loadCall.isUniquelyIdentified() || (0, Util_1.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 }));
|
|
43
|
+
const preserveOptions = (typeof this.options.preserve === "object") ? this.options.preserve : {};
|
|
44
|
+
result = (0, preserve_1._preserve)(lastResult, result, preserveOptions, { callStack: this.loadCall.diagnosis_callstack });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Save lastresult:
|
|
48
|
+
if (this.options.preserve !== false || this.options.silent) { // last result will be needed later?
|
|
49
|
+
this.loadCall.lastResult = result; // save for later
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Be memory friendly and don't leak references.
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (this.options.fixedInterval === false)
|
|
58
|
+
this.lastExecTime = new Date(); // Take timestamp
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
activateRegularRePollingIfNeeded() {
|
|
62
|
+
// Check, if we should really schedule:
|
|
63
|
+
this.checkValid();
|
|
64
|
+
if (!this.options.interval) { // Polling not enabled ?
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (this.rePollTimer !== undefined) { // Already scheduled ?
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (this.isObsolete) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (this.result.state === "pending") {
|
|
74
|
+
return; // will call activateRegularRePollingIfNeeded() when load is finished and a rerender is done
|
|
75
|
+
}
|
|
76
|
+
this.rePollTimer = setTimeout(async () => {
|
|
77
|
+
// Check, if we should really execute:
|
|
78
|
+
this.checkValid();
|
|
79
|
+
if (this.rePollTimer === undefined) { // Not scheduled anymore / frame not alive?
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.isObsolete) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await this.executeRePoll();
|
|
86
|
+
// Now that some time may have passed, check, again, if we should really schedule the next poll:
|
|
87
|
+
this.checkValid();
|
|
88
|
+
if (this.rePollTimer === undefined) { // Not scheduled anymore / frame not alive?
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (this.isObsolete) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Re-schedule
|
|
95
|
+
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
|
|
96
|
+
this.rePollTimer = undefined;
|
|
97
|
+
this.activateRegularRePollingIfNeeded();
|
|
98
|
+
}, Math.max(0, this.options.interval - (new Date().getTime() - this.lastExecTime.getTime())));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Re runs loaderFn
|
|
102
|
+
*/
|
|
103
|
+
async executeRePoll() {
|
|
104
|
+
try {
|
|
105
|
+
const value = await this.exec();
|
|
106
|
+
const isChanged = !(this.result.state === "resolved" && value === this.result.resolvedValue);
|
|
107
|
+
this.result = { state: "resolved", resolvedValue: value };
|
|
108
|
+
if (this.isObsolete) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (isChanged) {
|
|
112
|
+
this.loadCall.watchedComponentPersistent.handleChangeEvent(); // requests a re-render
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this.options.critical === false && (0, Util_1.isObject)(value)) {
|
|
116
|
+
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>
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
this.result = { state: "rejected", rejectReason: e };
|
|
122
|
+
if (!this.isObsolete) {
|
|
123
|
+
this.loadCall.watchedComponentPersistent.handleChangeEvent();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
deactivateRegularRePoll() {
|
|
128
|
+
this.checkValid();
|
|
129
|
+
if (this.rePollTimer !== undefined) {
|
|
130
|
+
clearTimeout(this.rePollTimer);
|
|
131
|
+
this.rePollTimer = undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
checkValid() {
|
|
135
|
+
if (this.rePollTimer !== undefined && this.result.state === "pending") {
|
|
136
|
+
throw new Error("Illegal state");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
constructor(loadCall, loaderFn, cache_index) {
|
|
140
|
+
this.debug_id = ++debug_idGenerator;
|
|
141
|
+
this.loadCall = loadCall;
|
|
142
|
+
this.loaderFn = loaderFn;
|
|
143
|
+
this.cache_index = cache_index;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Uniquely identifies a call, to remember the lastResult to preserve object instances
|
|
148
|
+
*/
|
|
149
|
+
class LoadCall {
|
|
150
|
+
constructor(id, watchedComponentPersistent, options, diagnosis_callStack, diagnosis_callerSourceLocation) {
|
|
151
|
+
this.id = id;
|
|
152
|
+
this.watchedComponentPersistent = watchedComponentPersistent;
|
|
153
|
+
this.options = options;
|
|
154
|
+
this.diagnosis_callstack = diagnosis_callStack;
|
|
155
|
+
this.diagnosis_callerSourceLocation = diagnosis_callerSourceLocation;
|
|
156
|
+
}
|
|
157
|
+
isUniquelyIdentified() {
|
|
158
|
+
let registeredForId = this.watchedComponentPersistent.loadCalls.get(this.id);
|
|
159
|
+
if (registeredForId === undefined) {
|
|
160
|
+
throw new Error("Illegal state: No Load call for this id was registered");
|
|
161
|
+
}
|
|
162
|
+
if (registeredForId === null) {
|
|
163
|
+
return false; // Null means: Not unique
|
|
164
|
+
}
|
|
165
|
+
if (registeredForId !== this) {
|
|
166
|
+
throw new Error("Illegal state: A different load call for this id was registered.");
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Value from the {@link LoadOptions#fallback} or through the {@link LoadOptions#silent} mechanism.
|
|
172
|
+
* Undefined, when no such "fallback" is available
|
|
173
|
+
*/
|
|
174
|
+
getFallbackValue() {
|
|
175
|
+
!(this.options.silent && !this.isUniquelyIdentified()) || (0, Util_1.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
|
|
176
|
+
if (this.options.silent && this.lastResult !== undefined) {
|
|
177
|
+
return { value: this.lastResult };
|
|
178
|
+
}
|
|
179
|
+
else if (this.options.hasOwnProperty("fallback")) {
|
|
180
|
+
return { value: this.options.fallback };
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Fields that persist across re-render and across frames
|
|
187
|
+
*/
|
|
188
|
+
class WatchedComponentPersistent {
|
|
189
|
+
doReRender() {
|
|
190
|
+
// Call listeners:
|
|
191
|
+
this.onceOnReRenderListeners.forEach(fn => fn());
|
|
192
|
+
this.onceOnReRenderListeners = [];
|
|
193
|
+
this._doReRender();
|
|
194
|
+
}
|
|
195
|
+
requestReRender(passiveFromRenderRun) {
|
|
196
|
+
const wasAlreadyRequested = this.reRenderRequested !== false;
|
|
197
|
+
// Enable the reRenderRequested flag:
|
|
198
|
+
if (passiveFromRenderRun !== undefined && this.reRenderRequested !== true) {
|
|
199
|
+
this.reRenderRequested = passiveFromRenderRun;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.reRenderRequested = true;
|
|
203
|
+
}
|
|
204
|
+
if (wasAlreadyRequested) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// Do the re-render:
|
|
208
|
+
if (currentRenderRun !== undefined) {
|
|
209
|
+
// Must defer it because we cannot call rerender from inside a render function
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
this.doReRender();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
this.doReRender();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* When a load finished or finished with error, or when a watched value changed. So the component needs to be rerendered
|
|
220
|
+
*/
|
|
221
|
+
handleChangeEvent() {
|
|
222
|
+
var _a, _b;
|
|
223
|
+
(_b = (_a = this.currentFrame).dismissErrorBoundary) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
224
|
+
this.requestReRender();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* @returns boolean it looks like ... (passive is very shy) unless another render run in the meanwhile or a non-passive rerender request will dominate
|
|
228
|
+
*/
|
|
229
|
+
nextReRenderMightBePassive() {
|
|
230
|
+
var _a;
|
|
231
|
+
return ((_a = this.currentFrame) === null || _a === void 0 ? void 0 : _a.recentRenderRun) !== undefined && this.reRenderRequested === this.currentFrame.recentRenderRun;
|
|
232
|
+
}
|
|
233
|
+
constructor(options) {
|
|
234
|
+
/**
|
|
235
|
+
* 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.
|
|
236
|
+
*/
|
|
237
|
+
this.watchedProps = getWatchedProxyFacade().getProxyFor({});
|
|
238
|
+
/**
|
|
239
|
+
* id -> loadCall. Null when there are multiple for that id
|
|
240
|
+
*/
|
|
241
|
+
this.loadCalls = new Map();
|
|
242
|
+
/**
|
|
243
|
+
* LoadRuns in the exact order, they occur
|
|
244
|
+
*/
|
|
245
|
+
this.loadRuns = [];
|
|
246
|
+
/**
|
|
247
|
+
* - true = rerender requested (will re-render asap) or just starting the render and changes in props/state/watched still make it into it.
|
|
248
|
+
* - false = ...
|
|
249
|
+
* - RenderRun = A passive render is requested. Save reference to the render run as safety check
|
|
250
|
+
*/
|
|
251
|
+
this.reRenderRequested = false;
|
|
252
|
+
this.hadASuccessfullMount = false;
|
|
253
|
+
/**
|
|
254
|
+
* Called either before or on the render
|
|
255
|
+
*/
|
|
256
|
+
this.onceOnReRenderListeners = [];
|
|
257
|
+
this.onceOnEffectCleanupListeners = [];
|
|
258
|
+
this.options = options;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
*
|
|
262
|
+
* @param props
|
|
263
|
+
*/
|
|
264
|
+
applyNewProps(props) {
|
|
265
|
+
// Set / add new props:
|
|
266
|
+
for (const key in props) {
|
|
267
|
+
//@ts-ignore
|
|
268
|
+
this.watchedProps[key] = props[key];
|
|
269
|
+
}
|
|
270
|
+
// Set non-existing to undefined:
|
|
271
|
+
for (const key in this.watchedProps) {
|
|
272
|
+
//@ts-ignore
|
|
273
|
+
this.watchedProps[key] = props[key];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
*
|
|
279
|
+
* Lifecycle: Render + optional passive render + timespan until the next render (=because something new happened) or until the final unmount.
|
|
280
|
+
* Note: In case of an error and wrapped in a recoverable <ErrorBoundary>, the may not even be a mount but this Frame still exist.
|
|
281
|
+
*
|
|
282
|
+
*/
|
|
283
|
+
class Frame {
|
|
284
|
+
//watchedProxyFacade= new WatchedProxyFacade();
|
|
285
|
+
get watchedProxyFacade() {
|
|
286
|
+
// 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
|
|
287
|
+
return getWatchedProxyFacade();
|
|
288
|
+
}
|
|
289
|
+
constructor() {
|
|
290
|
+
this.isListeningForChanges = false;
|
|
291
|
+
this.startPropChangeListeningFns = [];
|
|
292
|
+
this.cleanUpPropChangeListenerFns = [];
|
|
293
|
+
this.watchPropertyChange_changeListenerFn = this.watchPropertyChange_changeListenerFn.bind(this); // method is handed over as function but uses "this" inside.
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Makes the frame become "alive". Listens for property changes and re-polls poll(...) statements.
|
|
297
|
+
* Calling it twice does not hurt.
|
|
298
|
+
*/
|
|
299
|
+
startListeningForChanges() {
|
|
300
|
+
if (this.isListeningForChanges) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.startPropChangeListeningFns.forEach(c => c());
|
|
304
|
+
this.persistent.loadRuns.forEach(lc => lc.activateRegularRePollingIfNeeded()); // Schedule re-polls
|
|
305
|
+
this.isListeningForChanges = true;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* @see startListeningForChanges
|
|
309
|
+
* @param deactivateRegularRePoll keep this true normally.
|
|
310
|
+
*/
|
|
311
|
+
stopListeningForChanges(deactivateRegularRePoll = true) {
|
|
312
|
+
if (!this.isListeningForChanges) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
this.cleanUpPropChangeListenerFns.forEach(c => c()); // Clean the listeners
|
|
316
|
+
if (deactivateRegularRePoll) {
|
|
317
|
+
this.persistent.loadRuns.forEach(lc => lc.deactivateRegularRePoll()); // Stop scheduled re-polls
|
|
318
|
+
}
|
|
319
|
+
this.isListeningForChanges = false;
|
|
320
|
+
}
|
|
321
|
+
handleWatchedPropertyChange() {
|
|
322
|
+
this.persistent.handleChangeEvent();
|
|
323
|
+
}
|
|
324
|
+
watchPropertyChange(read) {
|
|
325
|
+
//Diagnosis: Provoke errors early, cause the code at the bottom of this method looses the stacktrace to the user's jsx
|
|
326
|
+
if (this.persistent.options.watchOutside !== false) {
|
|
327
|
+
try {
|
|
328
|
+
if (read instanceof proxy_facades_1.RecordedReadOnProxiedObject) {
|
|
329
|
+
(0, proxy_facades_1.installChangeTracker)(read.obj);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
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 });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Re-render on a change of the read value:
|
|
337
|
+
this.startPropChangeListeningFns.push(() => read.onAfterChange(this.watchPropertyChange_changeListenerFn /* Performance: We're not using an anonymous(=instance-changing) function here */, this.persistent.options.watchOutside !== false));
|
|
338
|
+
this.cleanUpPropChangeListenerFns.push(() => read.offAfterChange(this.watchPropertyChange_changeListenerFn /* Performance: We're not using an anonymous(=instance-changing) function here */));
|
|
339
|
+
}
|
|
340
|
+
watchPropertyChange_changeListenerFn() {
|
|
341
|
+
if (currentRenderRun) {
|
|
342
|
+
throw new Error("You must not modify a watched object during the render run.");
|
|
343
|
+
}
|
|
344
|
+
this.handleWatchedPropertyChange();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Lifecycle: Starts when rendering and ends when unmounting or re-rendering the watchedComponent.
|
|
349
|
+
* - References to this can still exist when WatchedComponentPersistent is in a resumeable error state (is this a good idea? )
|
|
350
|
+
*/
|
|
351
|
+
class RenderRun {
|
|
352
|
+
constructor() {
|
|
353
|
+
this.isPassive = false;
|
|
354
|
+
this.recordedReads = [];
|
|
355
|
+
this.loadCallIdsSeen = new Set();
|
|
356
|
+
/**
|
|
357
|
+
* Increased, when we see a load(...) call
|
|
358
|
+
*/
|
|
359
|
+
this.loadCallIndex = 0;
|
|
360
|
+
this.onFinallyAfterUsersComponentFnListeners = [];
|
|
361
|
+
this.somePendingAreCritical = false;
|
|
362
|
+
}
|
|
363
|
+
handleRenderFinishedSuccessfully() {
|
|
364
|
+
if (!this.isPassive) {
|
|
365
|
+
// Delete unused loadCalls
|
|
366
|
+
const keys = [...this.frame.persistent.loadCalls.keys()];
|
|
367
|
+
keys.forEach(key => {
|
|
368
|
+
if (!this.loadCallIdsSeen.has(key)) {
|
|
369
|
+
this.frame.persistent.loadCalls.delete(key);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
// Delete unused loadRuns:
|
|
373
|
+
this.frame.persistent.loadRuns = this.frame.persistent.loadRuns.slice(0, this.loadCallIndex + 1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Body of useEffect
|
|
378
|
+
*/
|
|
379
|
+
handleEffectSetup() {
|
|
380
|
+
this.frame.persistent.hadASuccessfullMount = true;
|
|
381
|
+
this.frame.startListeningForChanges();
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Called by useEffect before the next render oder before unmount(for suspense, for error or forever)
|
|
385
|
+
*/
|
|
386
|
+
handleEffectCleanup() {
|
|
387
|
+
// Call listeners:
|
|
388
|
+
this.frame.persistent.onceOnEffectCleanupListeners.forEach(fn => fn());
|
|
389
|
+
this.frame.persistent.onceOnEffectCleanupListeners = [];
|
|
390
|
+
let currentFrame = this.frame.persistent.currentFrame;
|
|
391
|
+
if (currentFrame.result instanceof Error && currentFrame.dismissErrorBoundary !== undefined) { // Error is displayed ?
|
|
392
|
+
// Still listen for property changes to be able to recover from errors and clean up later:
|
|
393
|
+
if (this.frame !== currentFrame) { // this.frame is old ?
|
|
394
|
+
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)
|
|
395
|
+
}
|
|
396
|
+
this.frame.persistent.onceOnReRenderListeners.push(() => {
|
|
397
|
+
this.frame.stopListeningForChanges();
|
|
398
|
+
this.frame.persistent.loadRuns.forEach(lc => lc.deactivateRegularRePoll()); // hacky solution2: The lines above have propably skipped this, so do it now
|
|
399
|
+
}); //Instead clean up listeners next time
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
this.frame.stopListeningForChanges(); // Clean up now
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
let currentRenderRun;
|
|
407
|
+
function watchedComponent(componentFn, options = {}) {
|
|
408
|
+
const outerResult = (props) => {
|
|
409
|
+
const [renderCounter, setRenderCounter] = (0, react_1.useState)(0);
|
|
410
|
+
const [persistent] = (0, react_1.useState)(new WatchedComponentPersistent(options));
|
|
411
|
+
persistent._doReRender = () => setRenderCounter(renderCounter + 1);
|
|
412
|
+
const isPassive = persistent.nextReRenderMightBePassive();
|
|
413
|
+
// Apply the new props (may trigger change listeners and therefore requestReRender() )
|
|
414
|
+
persistent.reRenderRequested = true; // this prevents new re-renders
|
|
415
|
+
persistent.requestReRender(); // Test, that this does not cause an infinite loop. (line can be removed when running stable)
|
|
416
|
+
persistent.applyNewProps(props);
|
|
417
|
+
persistent.reRenderRequested = false;
|
|
418
|
+
// Call remaining listeners, because may be the render was not "requested" through code in this package but happened some other way:
|
|
419
|
+
persistent.onceOnReRenderListeners.forEach(fn => fn());
|
|
420
|
+
persistent.onceOnReRenderListeners = [];
|
|
421
|
+
// Create frame:
|
|
422
|
+
let frame = isPassive && persistent.currentFrame !== undefined ? persistent.currentFrame : new Frame();
|
|
423
|
+
persistent.currentFrame = frame;
|
|
424
|
+
frame.persistent = persistent;
|
|
425
|
+
// Create RenderRun:
|
|
426
|
+
currentRenderRun === undefined || (0, Util_1.throwError)("Illegal state: already in currentRenderRun");
|
|
427
|
+
const renderRun = currentRenderRun = new RenderRun();
|
|
428
|
+
renderRun.frame = frame;
|
|
429
|
+
renderRun.isPassive = isPassive;
|
|
430
|
+
frame.recentRenderRun = currentRenderRun;
|
|
431
|
+
// Register dismissErrorBoundary function:
|
|
432
|
+
if (typeof react_error_boundary_1.useErrorBoundary === "function") { // Optional package was loaded?
|
|
433
|
+
if ((0, react_1.useContext)(react_error_boundary_1.ErrorBoundaryContext)) { // Inside an error boundary?
|
|
434
|
+
frame.dismissErrorBoundary = (0, react_error_boundary_1.useErrorBoundary)().resetBoundary;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
(0, react_1.useEffect)(() => {
|
|
438
|
+
renderRun.handleEffectSetup();
|
|
439
|
+
return () => renderRun.handleEffectCleanup();
|
|
440
|
+
});
|
|
441
|
+
try {
|
|
442
|
+
// Install read listener:
|
|
443
|
+
let readListener = (read) => {
|
|
444
|
+
if (!renderRun.isPassive) { // Active run ?
|
|
445
|
+
frame.watchPropertyChange(read);
|
|
446
|
+
}
|
|
447
|
+
renderRun.recordedReads.push(read);
|
|
448
|
+
};
|
|
449
|
+
frame.watchedProxyFacade.onAfterRead(readListener);
|
|
450
|
+
try {
|
|
451
|
+
try {
|
|
452
|
+
let result = componentFn(persistent.watchedProps); // Run the user's component function
|
|
453
|
+
renderRun.handleRenderFinishedSuccessfully();
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
renderRun.onFinallyAfterUsersComponentFnListeners.forEach(l => l()); // Call listeners
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
if (persistent.nextReRenderMightBePassive()) {
|
|
462
|
+
return (0, react_1.createElement)(react_1.Fragment, null); // Don't go to suspense **now**. The passive render might have a different outcome. (rerender will be done, see "finally")
|
|
463
|
+
}
|
|
464
|
+
frame.result = e;
|
|
465
|
+
if (e instanceof Promise) {
|
|
466
|
+
if (!persistent.hadASuccessfullMount) {
|
|
467
|
+
// Handle the suspense ourself. Cause the react Suspense does not restore the state by useState :(
|
|
468
|
+
return (0, react_1.createElement)(react_1.Fragment, null); // Return an empty element (might cause a short screen flicker) and render again.
|
|
469
|
+
}
|
|
470
|
+
if (options.fallback) {
|
|
471
|
+
return options.fallback;
|
|
472
|
+
}
|
|
473
|
+
// React's <Suspense> seems to keep this component mounted (hidden), so here's no need for an artificial renderRun.startListeningForChanges();
|
|
474
|
+
}
|
|
475
|
+
else { // Error?
|
|
476
|
+
if (frame.dismissErrorBoundary !== undefined) { // inside (recoverable) error boundary ?
|
|
477
|
+
// The useEffects won't fire, so whe simulate the frame's effect lifecycle here:
|
|
478
|
+
frame.startListeningForChanges();
|
|
479
|
+
persistent.onceOnReRenderListeners.push(() => { frame.stopListeningForChanges(); });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
throw e;
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
frame.watchedProxyFacade.offAfterRead(readListener);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
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
|
|
490
|
+
currentRenderRun = undefined;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
if (options.memo === false) {
|
|
494
|
+
return outerResult;
|
|
495
|
+
}
|
|
496
|
+
return (0, react_1.memo)(outerResult);
|
|
497
|
+
}
|
|
498
|
+
function watched(obj, options) {
|
|
499
|
+
currentRenderRun || (0, Util_1.throwError)("watched is not used from inside a watchedComponent");
|
|
500
|
+
return currentRenderRun.frame.watchedProxyFacade.getProxyFor(obj);
|
|
501
|
+
}
|
|
502
|
+
function useWatchedState(initial, options) {
|
|
503
|
+
currentRenderRun || (0, Util_1.throwError)("useWatchedState is not used from inside a watchedComponent");
|
|
504
|
+
const [state] = (0, react_1.useState)(initial);
|
|
505
|
+
return watched(state);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* 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.
|
|
509
|
+
* <p>
|
|
510
|
+
* Opposed to {@link load}, it does not treat all previously accessed properties as dependencies
|
|
511
|
+
* </p>
|
|
512
|
+
* <p>
|
|
513
|
+
* Immediately means: Before the promise is returned. I.e. does not record any more after your fetch finished.
|
|
514
|
+
* </p>
|
|
515
|
+
* @param loader
|
|
516
|
+
*/
|
|
517
|
+
function useLoad(loader) {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* 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.
|
|
522
|
+
* 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).
|
|
523
|
+
* <p>
|
|
524
|
+
* {@link https://github.com/bogeeee/react-deepwatch#and-less-loading-code Usage}.
|
|
525
|
+
* </p>
|
|
526
|
+
* @param loaderFn
|
|
527
|
+
* @param options
|
|
528
|
+
*/
|
|
529
|
+
function load(loaderFn, options = {}) {
|
|
530
|
+
const callStack = new Error("load(...) was called"); // Look not here, but one level down in the stack, where you called load(...)
|
|
531
|
+
// Wording:
|
|
532
|
+
// - "previous" means: load(...) statements more upwards in the user's code
|
|
533
|
+
// - "last" means: this load call but from a past frame
|
|
534
|
+
// Validity checks:
|
|
535
|
+
typeof loaderFn === "function" || (0, Util_1.throwError)("loaderFn is not a function");
|
|
536
|
+
if (currentRenderRun === undefined)
|
|
537
|
+
throw new Error("load is not used from inside a watchedComponent");
|
|
538
|
+
const renderRun = currentRenderRun;
|
|
539
|
+
const frame = renderRun.frame;
|
|
540
|
+
const persistent = frame.persistent;
|
|
541
|
+
const recordedReadsSincePreviousLoadCall = renderRun.recordedReads;
|
|
542
|
+
renderRun.recordedReads = []; // Pop recordedReads
|
|
543
|
+
const callerSourceLocation = callStack.stack ? getCallerSourceLocation(callStack.stack) : undefined;
|
|
544
|
+
// Determine loadCallId:
|
|
545
|
+
let loadCallId;
|
|
546
|
+
if (options.id !== undefined) {
|
|
547
|
+
options.key === undefined || (0, Util_1.throwError)("Must not set both: LoadOptions#id and LoadOptions#key"); // Validity check
|
|
548
|
+
loadCallId = options.id;
|
|
549
|
+
!renderRun.loadCallIdsSeen.has(loadCallId) || (0, Util_1.throwError)(`LoadOptions#id=${loadCallId} is not unique`);
|
|
550
|
+
}
|
|
551
|
+
else if (options.key !== undefined) {
|
|
552
|
+
callerSourceLocation || (0, Util_1.throwError)("No callstack available to compose the id. Please specify LoadOptions#id instead of LoadOptions#key"); // validity check
|
|
553
|
+
loadCallId = `${callerSourceLocation}___${options.key}`; // I.e. ...
|
|
554
|
+
!renderRun.loadCallIdsSeen.has(loadCallId) || (0, Util_1.throwError)(`LoadOptions#key=${options.key} is used multiple times / is not unique here.`);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
loadCallId = callerSourceLocation; // from source location only
|
|
558
|
+
}
|
|
559
|
+
const isUnique = !(renderRun.loadCallIdsSeen.has(loadCallId) || persistent.loadCalls.get(loadCallId) === null);
|
|
560
|
+
renderRun.loadCallIdsSeen.add(loadCallId);
|
|
561
|
+
// Find the loadCall or create it:
|
|
562
|
+
let loadCall;
|
|
563
|
+
if (isUnique) {
|
|
564
|
+
loadCall = persistent.loadCalls.get(loadCallId);
|
|
565
|
+
}
|
|
566
|
+
if (loadCall === undefined) {
|
|
567
|
+
loadCall = new LoadCall(loadCallId, persistent, options, callStack, callerSourceLocation);
|
|
568
|
+
}
|
|
569
|
+
persistent.loadCalls.set(loadCallId, isUnique ? loadCall : null);
|
|
570
|
+
loadCall.options = options; // Update options. It is allowed that these can change over time. I.e. the poll interval or the name.
|
|
571
|
+
// Determine lastLoadRun:
|
|
572
|
+
let lastLoadRun = renderRun.loadCallIndex < persistent.loadRuns.length ? persistent.loadRuns[renderRun.loadCallIndex] : undefined;
|
|
573
|
+
if (lastLoadRun) {
|
|
574
|
+
lastLoadRun.loaderFn = options.interval ? loaderFn : undefined; // Update. only needed, when polling.
|
|
575
|
+
lastLoadRun.loadCall.id === loadCall.id || (0, Util_1.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
|
|
576
|
+
}
|
|
577
|
+
const fallback = loadCall.getFallbackValue();
|
|
578
|
+
try {
|
|
579
|
+
if (renderRun.isPassive) {
|
|
580
|
+
// Don't look at recorded reads. Assume the order has not changed
|
|
581
|
+
// Validity check:
|
|
582
|
+
if (lastLoadRun === undefined) {
|
|
583
|
+
//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.");
|
|
584
|
+
// 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
|
|
585
|
+
if (fallback) {
|
|
586
|
+
return fallback.value;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
throw new Error(`When using isLoading(), you must specify fallbacks for all your load statements: load(..., {fallback: some-fallback-value})`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
//** return lastLoadRun.result:
|
|
593
|
+
if (lastLoadRun.result.state === "resolved") {
|
|
594
|
+
return options.critical !== false ? watched(lastLoadRun.result.resolvedValue) : lastLoadRun.result.resolvedValue;
|
|
595
|
+
}
|
|
596
|
+
else if ((lastLoadRun === null || lastLoadRun === void 0 ? void 0 : lastLoadRun.result.state) === "rejected") {
|
|
597
|
+
throw lastLoadRun.result.rejectReason;
|
|
598
|
+
}
|
|
599
|
+
else if (lastLoadRun.result.state === "pending") {
|
|
600
|
+
if (fallback) {
|
|
601
|
+
return fallback.value;
|
|
602
|
+
}
|
|
603
|
+
throw lastLoadRun.result.promise;
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
throw new Error("Unhandled state");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
let result = inner();
|
|
610
|
+
if (options.critical === false) {
|
|
611
|
+
return result; // non-watched and add no dependency
|
|
612
|
+
}
|
|
613
|
+
renderRun.recordedReads.push(new proxy_facades_1.RecordedValueRead(result)); // Add as dependency for the next loads
|
|
614
|
+
return watched(result);
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
renderRun.loadCallIndex++;
|
|
618
|
+
}
|
|
619
|
+
function inner() {
|
|
620
|
+
const recordedReadsAreEqualSinceLastCall = lastLoadRun && (0, Util_1.recordedReadsArraysAreEqual)(recordedReadsSincePreviousLoadCall, lastLoadRun.recordedReadsBefore);
|
|
621
|
+
if (!recordedReadsAreEqualSinceLastCall) {
|
|
622
|
+
persistent.loadRuns = persistent.loadRuns.slice(0, renderRun.loadCallIndex); // Erase all loadRuns after here (including this one).
|
|
623
|
+
lastLoadRun = undefined;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Can we use the result from last call ?
|
|
627
|
+
*/
|
|
628
|
+
const canReuseLastResult = () => {
|
|
629
|
+
if (!lastLoadRun) { // call was not recorded last render or is invalid?
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
if (!recordedReadsAreEqualSinceLastCall) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
if (lastLoadRun.recordedReadsInsideLoaderFn.some((r => r.isChanged))) { // I.e for "load( () => { fetch(props.x, myLocalValue) }) )" -> props.x or myLocalValue has changed?
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
if (lastLoadRun.result.state === "resolved") {
|
|
639
|
+
return { result: lastLoadRun.result.resolvedValue };
|
|
640
|
+
}
|
|
641
|
+
if (lastLoadRun.result.state === "pending") {
|
|
642
|
+
renderRun.somePending = lastLoadRun.result.promise;
|
|
643
|
+
renderRun.somePendingAreCritical || (renderRun.somePendingAreCritical = options.critical !== false);
|
|
644
|
+
if (fallback) { // Fallback available ?
|
|
645
|
+
return { result: fallback.value };
|
|
646
|
+
}
|
|
647
|
+
lastLoadRun.recordedReadsInsideLoaderFn.forEach(read => frame.watchPropertyChange(read)); // Also watch recordedReadsInsideLoaderFn (again in this frame)
|
|
648
|
+
throw lastLoadRun.result.promise; // Throwing a promise will put the react component into suspense state
|
|
649
|
+
}
|
|
650
|
+
else if (lastLoadRun.result.state === "rejected") {
|
|
651
|
+
lastLoadRun.recordedReadsInsideLoaderFn.forEach(read => frame.watchPropertyChange(read)); // Also watch recordedReadsInsideLoaderFn (again in this frame)
|
|
652
|
+
throw lastLoadRun.result.rejectReason;
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
throw new Error("Invalid state of lastLoadRun.result.state");
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const canReuse = canReuseLastResult();
|
|
659
|
+
if (canReuse !== false) { // can re-use ?
|
|
660
|
+
const lastCall = persistent.loadRuns[renderRun.loadCallIndex];
|
|
661
|
+
lastCall.recordedReadsInsideLoaderFn.forEach(read => frame.watchPropertyChange(read)); // Also watch recordedReadsInsideLoaderFn
|
|
662
|
+
return canReuse.result; // return proxy'ed result from last call:
|
|
663
|
+
}
|
|
664
|
+
else { // cannot use last result ?
|
|
665
|
+
if (renderRun.somePending && renderRun.somePendingAreCritical) { // Performance: Some previous (and dependent) results are pending, so loading this one would trigger a reload soon
|
|
666
|
+
// don't make a new call
|
|
667
|
+
if (fallback) {
|
|
668
|
+
return fallback.value;
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
throw renderRun.somePending;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// *** make a loadRun / exec loaderFn ***:
|
|
675
|
+
let loadRun = new LoadRun(loadCall, loaderFn, renderRun.loadCallIndex);
|
|
676
|
+
loadRun.recordedReadsBefore = recordedReadsSincePreviousLoadCall;
|
|
677
|
+
const resultPromise = Promise.resolve(loadRun.exec()); // Exec loaderFn
|
|
678
|
+
loadRun.loaderFn = options.interval ? loadRun.loaderFn : undefined; // Remove reference if not needed to not risk leaking memory
|
|
679
|
+
loadRun.recordedReadsInsideLoaderFn = renderRun.recordedReads;
|
|
680
|
+
renderRun.recordedReads = []; // pop and remember the (immediate) reads from inside the loaderFn
|
|
681
|
+
resultPromise.then((value) => {
|
|
682
|
+
loadRun.result = { state: "resolved", resolvedValue: value };
|
|
683
|
+
if (loadRun.isObsolete) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
/*
|
|
687
|
+
const otherLoadsAreWaiting=true;// Other loads are waiting for this critical loadRun? TODO
|
|
688
|
+
const wasErrored = true; // TODO
|
|
689
|
+
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?
|
|
690
|
+
// Not worth it / too risky, just to save one rerender. Maybe i have overseen something.
|
|
691
|
+
if(persistent.currentFrame?.isListeningForChanges) { // Frame is "alive" ?
|
|
692
|
+
loadRun.activateRegularRePollingIfNeeded();
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
persistent.handleChangeEvent(); // Will also do a rerender and call activateRegularRePollingIfNeeded, like above
|
|
696
|
+
}
|
|
697
|
+
*/
|
|
698
|
+
persistent.handleChangeEvent();
|
|
699
|
+
});
|
|
700
|
+
resultPromise.catch(reason => {
|
|
701
|
+
loadRun.result = { state: "rejected", rejectReason: reason };
|
|
702
|
+
if (loadRun.isObsolete) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
persistent.handleChangeEvent(); // Re-render. The next render will see state=rejected for this load statement and throw it then.
|
|
706
|
+
});
|
|
707
|
+
loadRun.result = { state: "pending", promise: resultPromise };
|
|
708
|
+
persistent.loadRuns[renderRun.loadCallIndex] = loadRun; // add / replace
|
|
709
|
+
renderRun.somePending = resultPromise;
|
|
710
|
+
renderRun.somePendingAreCritical || (renderRun.somePendingAreCritical = options.critical !== false);
|
|
711
|
+
if (fallback) {
|
|
712
|
+
return fallback.value;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
throw resultPromise; // Throwing a promise will put the react component into suspense state
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function watched(value) { return (value !== null && typeof value === "object") ? frame.watchedProxyFacade.getProxyFor(value) : value; }
|
|
720
|
+
/**
|
|
721
|
+
*
|
|
722
|
+
* @param callStack
|
|
723
|
+
* @returns i.e. "at http://localhost:5173/index.tsx:98:399"
|
|
724
|
+
*/
|
|
725
|
+
function getCallerSourceLocation(callStack) {
|
|
726
|
+
callStack = callStack.replace(/.*load\(\.\.\.\) was called\s*/, ""); // Remove trailing error from callstack
|
|
727
|
+
const callStackRows = callStack.split("\n");
|
|
728
|
+
callStackRows.length >= 2 || (0, Util_1.throwError)(`Unexpected callstack format: ${callStack}`); // Validity check
|
|
729
|
+
let result = callStackRows[1].trim();
|
|
730
|
+
result !== "" || (0, Util_1.throwError)("Illegal result");
|
|
731
|
+
result = result.replace(/at\s*/, ""); // Remove trailing "at "
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Probe if a <code>load(...)</code> statement directly inside this watchedComponent is currently loading.
|
|
737
|
+
* 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.
|
|
738
|
+
* <p>
|
|
739
|
+
* Example. This uses isLoading() to determine if the Dropdown list should be faded/transparent during while items are loading:
|
|
740
|
+
* <pre><code>
|
|
741
|
+
* return <select style={{opacity: isLoading("dropdownItems")?0.5:1}}>
|
|
742
|
+
* {load(() => fetchMyDropdownItems(), {name: "dropdownItems", fallback: ["loading items"]}).map(i => <option value="{i}">{i}</option>)}
|
|
743
|
+
* </select>
|
|
744
|
+
* </code></pre>
|
|
745
|
+
* </p>
|
|
746
|
+
* <p>
|
|
747
|
+
* 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.
|
|
748
|
+
* </p>
|
|
749
|
+
* @param nameFilter When set, consider only those with the given {@link LoadOptions#name}. I.e. <code>load(..., {name: "myDropdownListEntries"})</code>
|
|
750
|
+
*
|
|
751
|
+
*/
|
|
752
|
+
function isLoading(nameFilter) {
|
|
753
|
+
const renderRun = currentRenderRun;
|
|
754
|
+
// Validity check:
|
|
755
|
+
if (renderRun === undefined)
|
|
756
|
+
throw new Error("isLoading is not used from inside a watchedComponent");
|
|
757
|
+
return probe(() => renderRun.frame.persistent.loadRuns.some(c => c.result.state === "pending" && (!nameFilter || c.name === nameFilter)), false);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Probe if a <code>load(...)</code> statement directly inside this watchedComponent failed.
|
|
761
|
+
* <p>
|
|
762
|
+
* Example:
|
|
763
|
+
* <pre><code>
|
|
764
|
+
* if(loadFailed()) {
|
|
765
|
+
* return <div>Load failed: {loadFailed().message}</div>;
|
|
766
|
+
* }
|
|
767
|
+
*
|
|
768
|
+
* return <div>My component content {load(...)} </div>
|
|
769
|
+
* </code></pre>
|
|
770
|
+
* </p>
|
|
771
|
+
* <p>
|
|
772
|
+
* 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.
|
|
773
|
+
* </p>
|
|
774
|
+
* @param nameFilter When set, consider only those with the given {@link LoadOptions#name}. I.e. <code>load(..., {name: "myDropdownListEntries"})</code>
|
|
775
|
+
* @returns unknown The thrown value of the loaderFn or undefined if everything is ok.
|
|
776
|
+
*/
|
|
777
|
+
function loadFailed(nameFilter) {
|
|
778
|
+
const renderRun = currentRenderRun;
|
|
779
|
+
// Validity check:
|
|
780
|
+
if (renderRun === undefined)
|
|
781
|
+
throw new Error("isLoading is not used from inside a watchedComponent");
|
|
782
|
+
return probe(() => {
|
|
783
|
+
var _a, _b;
|
|
784
|
+
return (_b = (_a = renderRun.frame.persistent.loadRuns.find(c => c.result.state === "rejected" && (!nameFilter || c.name === nameFilter))) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.rejectReason;
|
|
785
|
+
}, undefined);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Like {@link load}, but re-runs loaderFn regularly at the interval, specified in the options.
|
|
789
|
+
* <p>
|
|
790
|
+
* Example: <code>return <div>The current outside temperature is { poll( async () => await fetchTemperatureFromServer(), {interval: 1000} ) }° </div></code> *
|
|
791
|
+
* </p>
|
|
792
|
+
* <p>
|
|
793
|
+
* Polling is still continued in recoverable error cases, when
|
|
794
|
+
* </p>
|
|
795
|
+
* - loaderFn fails but your watchedComponent catches it and returns fine.
|
|
796
|
+
* - Your watchedComponent returns with an error(because of this loaderFn or some other reason) and it is wrapped in a react-error-boundary.
|
|
797
|
+
*
|
|
798
|
+
* <p>
|
|
799
|
+
* Note, that after the initial load, re-polling is done <strong>very silently</strong>. Meaning, there's no suspense / fallback / isLoading indicator involved.
|
|
800
|
+
* </p>
|
|
801
|
+
* @param loaderFn
|
|
802
|
+
* @param options
|
|
803
|
+
*/
|
|
804
|
+
function poll(loaderFn, options) {
|
|
805
|
+
return load(loaderFn, options);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* For isLoading and isError. Makes a passive render run if these a
|
|
809
|
+
* @param probeFn
|
|
810
|
+
* @param defaultResult
|
|
811
|
+
*/
|
|
812
|
+
function probe(probeFn, defaultResult) {
|
|
813
|
+
const renderRun = currentRenderRun;
|
|
814
|
+
// Validity check:
|
|
815
|
+
if (renderRun === undefined)
|
|
816
|
+
throw new Error("Not used from inside a watchedComponent");
|
|
817
|
+
if (renderRun.isPassive) {
|
|
818
|
+
return probeFn();
|
|
819
|
+
}
|
|
820
|
+
renderRun.onFinallyAfterUsersComponentFnListeners.push(() => {
|
|
821
|
+
if (probeFn() !== defaultResult) {
|
|
822
|
+
renderRun.frame.persistent.requestReRender(currentRenderRun); // Request passive render.
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
return defaultResult;
|
|
826
|
+
}
|
|
827
|
+
function debug_tagComponent(name) {
|
|
828
|
+
currentRenderRun.frame.persistent.debug_tag = name;
|
|
829
|
+
}
|
|
830
|
+
//# sourceMappingURL=index.js.map
|