@xmachines/play-dom 6.1.0 → 7.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/README.md +127 -48
- package/dist/PlayRenderer.d.ts +127 -32
- package/dist/PlayRenderer.d.ts.map +1 -1
- package/dist/PlayRenderer.js +584 -679
- package/dist/PlayRenderer.js.map +1 -1
- package/dist/create-play-ui.d.ts +3 -1
- package/dist/create-play-ui.d.ts.map +1 -1
- package/dist/create-play-ui.js +50 -94
- package/dist/create-play-ui.js.map +1 -1
- package/dist/create-renderer.js +37 -87
- package/dist/create-renderer.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -34
- package/dist/types.d.ts +23 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -19
- package/dist/index.js.map +0 -1
- package/dist/types.js +0 -10
- package/dist/types.js.map +0 -1
package/dist/PlayRenderer.js
CHANGED
|
@@ -1,689 +1,594 @@
|
|
|
1
|
-
|
|
2
|
-
* PlayRenderer.ts — the XMachines wrapper of @xmachines/json-render-dom.
|
|
3
|
-
*
|
|
4
|
-
* It connects actor.currentView, an atom, to the DOM renderer below it, with
|
|
5
|
-
* watchAtom. It gives you the connect() method and the disconnect() method, in
|
|
6
|
-
* the same pattern as the PlayRenderer class of play-react, of play-vue, and of
|
|
7
|
-
* play-solid.
|
|
8
|
-
*
|
|
9
|
-
* The state store: the renderer uses the external `store` option when the caller
|
|
10
|
-
* gives one, and this is the controlled mode. Without that option, it makes a new
|
|
11
|
-
* @xstate/store atom for each view transition, with the values of spec.state.
|
|
12
|
-
*/
|
|
13
|
-
import { createAtom, watchAtom } from "@xmachines/play-atom";
|
|
1
|
+
import { createAtom } from "@xmachines/play-atom";
|
|
14
2
|
import { xstateStoreStateStore } from "@xmachines/json-render-xstate";
|
|
15
|
-
import {
|
|
3
|
+
import { bindStoreHandlers, createReportGuard, createViewSession, forwardRenderError } from "@xmachines/play-view";
|
|
16
4
|
import { releaseQuietly } from "@xmachines/core/utils";
|
|
17
5
|
import { createIncrementalRenderer } from "@xmachines/json-render-dom";
|
|
6
|
+
//#region packages/play-dom/src/PlayRenderer.ts
|
|
7
|
+
/**
|
|
8
|
+
* PlayRenderer.ts — the XMachines wrapper of @xmachines/json-render-dom.
|
|
9
|
+
*
|
|
10
|
+
* It connects actor.currentView, an atom, to the DOM renderer below it, through the
|
|
11
|
+
* view session of @xmachines/play-view. It gives you the connect() method and the disconnect() method, in
|
|
12
|
+
* the same pattern as the PlayRenderer class of play-react, of play-vue, and of
|
|
13
|
+
* play-solid.
|
|
14
|
+
*
|
|
15
|
+
* The state store: the renderer uses the external `store` option when the caller
|
|
16
|
+
* gives one, and this is the controlled mode. Without that option, it makes a new
|
|
17
|
+
* @xstate/store atom for each view transition, with the values of spec.state.
|
|
18
|
+
*/
|
|
18
19
|
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
handlerThrew: "[@xmachines/play-dom] the onError handler of the host threw. " +
|
|
35
|
-
"PlayRenderer contained the failure of the view all the same.",
|
|
20
|
+
* What the report guard writes, and what it lets through.
|
|
21
|
+
*
|
|
22
|
+
* It CONTAINS a handler of the host that throws, exactly as the four framework renderers
|
|
23
|
+
* do. The five hold one rule, and a caller learns it one time: a handler of the host never
|
|
24
|
+
* defeats the containment. A host that must escalate raises the failure from a task of its
|
|
25
|
+
* own, which reaches the global handler of the page in every one of the five.
|
|
26
|
+
*
|
|
27
|
+
* It stands ABOVE the doc comment of the class, and so does every other declaration of
|
|
28
|
+
* this module: a declaration takes the comment block that stands directly before it, so a
|
|
29
|
+
* const or a function placed between that comment and the class takes the documentation
|
|
30
|
+
* of `PlayRenderer` with it, and the class reaches the generated reference with none.
|
|
31
|
+
*/
|
|
32
|
+
var GUARD_MESSAGES = {
|
|
33
|
+
noHandler: "[@xmachines/play-dom] PlayRenderer contained a failed rebuild of the view. Give an onError option to receive it.",
|
|
34
|
+
handlerThrew: "[@xmachines/play-dom] the onError handler of the host threw. PlayRenderer contained the failure of the view all the same."
|
|
36
35
|
};
|
|
37
36
|
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
* Reports a failure of one release of a caller, for `releaseQuietly` of `@xmachines/core/utils`.
|
|
38
|
+
*
|
|
39
|
+
* Every release that `disconnect()`, `render()`, and `resetFailedRebuild()` run is CODE
|
|
40
|
+
* OF A CALLER: the `unsubscribe` of a store that the host gave, and the watch cleanups
|
|
41
|
+
* that the inner renderer registered. Each one can throw, and a throw must stop no path.
|
|
42
|
+
* `render()` releases the registrations of the previous view on the path of a view that
|
|
43
|
+
* works, and the comment at that call gives its reason.
|
|
44
|
+
*
|
|
45
|
+
* `resetFailedRebuild()` runs the releases of a FAILED rebuild, BEFORE the renderer
|
|
46
|
+
* contains the failure of the view. A throw there takes the containment down with it: the
|
|
47
|
+
* catch that called it reaches `contain()` never, so the failure of the view reaches no
|
|
48
|
+
* handler, the dead tree stays on the screen, and the failure of the release escapes in
|
|
49
|
+
* its place — which is both outcomes that the containment exists to prevent.
|
|
50
|
+
*
|
|
51
|
+
* `disconnect()` has no containment to defeat, and it has the same rule for a second
|
|
52
|
+
* reason: a throw from the first release skips every release after it, it skips the clear
|
|
53
|
+
* of the container, and it skips the reset of the identity. The renderer then holds a
|
|
54
|
+
* half teardown that no caller can complete.
|
|
55
|
+
*
|
|
56
|
+
* Each release therefore stands alone, and a failure of one goes to `console.error`.
|
|
57
|
+
*/
|
|
59
58
|
function reportReleaseFailure(releaseFailure) {
|
|
60
|
-
|
|
61
|
-
"The renderer completed the teardown all the same.", releaseFailure);
|
|
59
|
+
console.error("[@xmachines/play-dom] a release of PlayRenderer threw. The renderer completed the teardown all the same.", releaseFailure);
|
|
62
60
|
}
|
|
63
61
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const loading = this.options.loading;
|
|
597
|
-
const functions = this.options.functions;
|
|
598
|
-
const directives = this.options.directives;
|
|
599
|
-
const validationFunctions = this.options.validationFunctions;
|
|
600
|
-
const navigate = this.options.navigate;
|
|
601
|
-
const onConfirm = this.options.onConfirm;
|
|
602
|
-
const onRenderError = this.options.onRenderError;
|
|
603
|
-
// ── The lifecycle of a watch ─────────────────────────────────────────────
|
|
604
|
-
// A watch subscription follows the MOUNT of an element, and the registry of each
|
|
605
|
-
// instance in the incremental renderer manages it: an element of the first render
|
|
606
|
-
// registers then, and an element that appears later, from a visibility that becomes
|
|
607
|
-
// true or from a repeat item that the code appends, registers at that moment, with
|
|
608
|
-
// the same skip-on-mount baseline. The renderer releases a subscription when its
|
|
609
|
-
// element unmounts, from a visibility that becomes false or from a repeat item that
|
|
610
|
-
// the code removes. It also releases each subscription on a view change, at the top of
|
|
611
|
-
// this method, and on a disconnect().
|
|
612
|
-
//
|
|
613
|
-
// A stable element does NOT receive a new watch on each rerender. The code registers
|
|
614
|
-
// the update subscription below with the store before the watch subscriptions of
|
|
615
|
-
// renderSpec, and @xstate/store notifies in the order of the insertion. If a render
|
|
616
|
-
// in place removed the watchers and made them again, each old watcher would die in
|
|
617
|
-
// the queue, before it fires, and each new watcher would take the snapshot after the
|
|
618
|
-
// update as its baseline. A watch action would then fire never. Therefore the registry
|
|
619
|
-
// keeps the ORIGINAL subscription, with its baseline and its position in the
|
|
620
|
-
// notification, whenever the code only renders an instance again. It registers a
|
|
621
|
-
// real new mount only.
|
|
622
|
-
//
|
|
623
|
-
// Each registered cleanup is idempotent, and the Set watchCleanups also holds it.
|
|
624
|
-
// Therefore a second call here, after the incremental renderer released it already, is
|
|
625
|
-
// safe.
|
|
626
|
-
const renderOptions = {
|
|
627
|
-
send,
|
|
628
|
-
handlers,
|
|
629
|
-
...(onRenderError !== undefined ? { onRenderError } : {}),
|
|
630
|
-
...(functions !== undefined ? { functions } : {}),
|
|
631
|
-
...(directives !== undefined ? { directives } : {}),
|
|
632
|
-
...(loading !== undefined ? { loading } : {}),
|
|
633
|
-
onWatchSetup: (cleanup) => {
|
|
634
|
-
// Wrap each cleanup. A call of the wrapper, from the disconnect() here or from the
|
|
635
|
-
// view change here, OR from the incremental renderer when the element of the cleanup
|
|
636
|
-
// unmounts, also removes the cleanup from the Set. The code returns the wrapper.
|
|
637
|
-
// Therefore the incremental renderer registers THIS wrapper and calls it on a
|
|
638
|
-
// release, and the Set stays in step with the live subscriptions.
|
|
639
|
-
const entry = () => {
|
|
640
|
-
this.watchCleanups.delete(entry);
|
|
641
|
-
cleanup();
|
|
642
|
-
};
|
|
643
|
-
this.watchCleanups.add(entry);
|
|
644
|
-
return entry;
|
|
645
|
-
},
|
|
646
|
-
...(validationFunctions !== undefined ? { validationFunctions } : {}),
|
|
647
|
-
...(navigate !== undefined ? { navigate } : {}),
|
|
648
|
-
...(onConfirm !== undefined ? { onConfirm } : {}),
|
|
649
|
-
};
|
|
650
|
-
// ── The incremental updates ─────────────────────────────────────────────
|
|
651
|
-
// A store update rebuilds the complete tree no longer: the incremental renderer
|
|
652
|
-
// compares the previous snapshot and the next snapshot on the tracked $state paths of
|
|
653
|
-
// each element, and it renders the elements of the change again, in place. An element
|
|
654
|
-
// without a state binding keeps the identity of its DOM node. Therefore the focus, the
|
|
655
|
-
// selection, and the scroll position survive an update of another element. A view
|
|
656
|
-
// transition still rebuilds everything, because this method runs again.
|
|
657
|
-
const incremental = createIncrementalRenderer(view, guardedStore, this.registry, this.container, renderOptions);
|
|
658
|
-
this.incremental = incremental;
|
|
659
|
-
this.lastRoot = view.root;
|
|
660
|
-
this.lastElements = view.elements;
|
|
661
|
-
// subscribe passes through the guard without a change, and it keeps the same function
|
|
662
|
-
// reference. The guard wraps set and update only.
|
|
663
|
-
this.storeUnsubscribe = guardedStore.subscribe(() => {
|
|
664
|
-
// The guard: stop when a caller disconnected the PlayRenderer.
|
|
665
|
-
// This handles the case of a StateStore implementation that fires its subscribed
|
|
666
|
-
// callbacks synchronously inside its own unsubscribe() call.
|
|
667
|
-
if (!this.alive)
|
|
668
|
-
return;
|
|
669
|
-
// The try CONTAINS a failed update, exactly as the try of render() contains a
|
|
670
|
-
// failed rebuild. `incremental.update()` resolves the props of every element that
|
|
671
|
-
// the write touched, and it rebuilds the COMPLETE tree when the store hands back
|
|
672
|
-
// the snapshot that it held — so a `$computed` function that throws on the new
|
|
673
|
-
// state aborts the render here. This callback runs from inside `store.update()`,
|
|
674
|
-
// which an ACTION HANDLER of the host calls, so such a throw left the renderer
|
|
675
|
-
// through a path that no option of this class covers: `onError` heard nothing,
|
|
676
|
-
// the dead tree stayed on the screen, and the failure reached the caller of the
|
|
677
|
-
// action instead. The containment of a failure of the view is one rule, and it
|
|
678
|
-
// holds on every path that renders.
|
|
679
|
-
try {
|
|
680
|
-
incremental.update();
|
|
681
|
-
}
|
|
682
|
-
catch (error) {
|
|
683
|
-
this.failView(error);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
incremental.render();
|
|
687
|
-
}
|
|
688
|
-
}
|
|
62
|
+
* PlayRenderer connects the `currentView` atom of an actor to the DOM renderer.
|
|
63
|
+
*
|
|
64
|
+
* It watches `actor.currentView` through the atoms. It renders each
|
|
65
|
+
* `DomComponentRenderer` function into `container` on every view transition.
|
|
66
|
+
* `disconnect()` clears the container.
|
|
67
|
+
*
|
|
68
|
+
* Every option of `PlayDomOptions`, which extends `UIProviderOptions`, goes into
|
|
69
|
+
* `DomRenderContext` on each render pass:
|
|
70
|
+
* - `functions` — the named compute functions of a `{ $computed: "name" }` prop expression
|
|
71
|
+
* - `directives` — your own dynamic values with a `$` prefix, from `defineDirective`. The renderer resolves them with the props
|
|
72
|
+
* - `validationFunctions` — your own check functions. They are available at `ctx.ctx.validationFunctions`
|
|
73
|
+
* - `navigate` — the navigation callback. The renderer calls it for an `onSuccess: { navigate: "..." }` action binding
|
|
74
|
+
* - `onRenderError` — the renderer calls it with `(error, name, info)` for each failure that the inner renderer reports
|
|
75
|
+
*
|
|
76
|
+
* **The preferred use, with `registryResult`:**
|
|
77
|
+
* ```typescript
|
|
78
|
+
* import { PlayRenderer, defineRegistry } from "@xmachines/play-dom";
|
|
79
|
+
*
|
|
80
|
+
* const registryResult = defineRegistry(catalog, { components, actions });
|
|
81
|
+
* const renderer = new PlayRenderer(container, actor, registryResult.registry, {
|
|
82
|
+
* registryResult, // it connects setState and getState of the xstate store for you
|
|
83
|
+
* navigate: (path) => myRouter.push(path),
|
|
84
|
+
* functions: { fullName: (args) => `${args.first} ${args.last}` },
|
|
85
|
+
* });
|
|
86
|
+
* renderer.connect();
|
|
87
|
+
* // Later:
|
|
88
|
+
* renderer.disconnect();
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* **The controlled store mode** — you give your own `StateStore`:
|
|
92
|
+
* ```typescript
|
|
93
|
+
* import { createAtom } from "@xmachines/play-atom";
|
|
94
|
+
* import { xstateStoreStateStore } from "@xmachines/json-render-xstate";
|
|
95
|
+
*
|
|
96
|
+
* const atom = createAtom({ username: "" });
|
|
97
|
+
* const store = xstateStoreStateStore({ atom });
|
|
98
|
+
* const renderer = new PlayRenderer(container, actor, registry, { registryResult, store });
|
|
99
|
+
* renderer.connect();
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* A second `connect()` is safe. A `connect()` call on a connected renderer
|
|
103
|
+
* disconnects it first. Therefore the renderer holds no second render
|
|
104
|
+
* subscription.
|
|
105
|
+
*/
|
|
106
|
+
var PlayRenderer = class {
|
|
107
|
+
container;
|
|
108
|
+
actor;
|
|
109
|
+
registry;
|
|
110
|
+
options;
|
|
111
|
+
/**
|
|
112
|
+
* True from the start of `connect()` to the end of `disconnect()`.
|
|
113
|
+
*
|
|
114
|
+
* The watcher cannot answer this question. `connect()` installs it AFTER the
|
|
115
|
+
* synchronous first render, so a host that calls `connect()` again from the `onError`
|
|
116
|
+
* of that render finds no watcher, and a test of the watcher alone would skip the
|
|
117
|
+
* `disconnect()` and leave the guard of the abandoned connection alive for ever.
|
|
118
|
+
*/
|
|
119
|
+
connected = false;
|
|
120
|
+
unwatch = null;
|
|
121
|
+
storeUnsubscribe = null;
|
|
122
|
+
/**
|
|
123
|
+
* The cleanup functions of the live watch subscriptions. This field is a `Set`,
|
|
124
|
+
* and not an array, so that each registered cleanup function can delete ITSELF on
|
|
125
|
+
* its call: the incremental renderer releases a watcher when its element unmounts,
|
|
126
|
+
* and it calls the registered wrapper. The Set therefore shrinks with the live
|
|
127
|
+
* subscriptions. It does not collect one spent closure for each historical mount of
|
|
128
|
+
* a view with a long life.
|
|
129
|
+
*/
|
|
130
|
+
watchCleanups = /* @__PURE__ */ new Set();
|
|
131
|
+
/**
|
|
132
|
+
* The generation of the store behind the tree on the screen, from the lifecycle. A
|
|
133
|
+
* resolution with the same generation keeps the store: only the /context projection
|
|
134
|
+
* moved, or a composed view changed its page, so the renderer keeps the tree and the
|
|
135
|
+
* ephemeral view state, such as a draft or a toggle. A new generation is a new store,
|
|
136
|
+
* and the renderer builds the tree again. A null emission, from a state with no
|
|
137
|
+
* meta.view, is a GAP, and not a new view: the DOM goes away, but the lifecycle keeps
|
|
138
|
+
* the store. A return to the same view therefore builds again around the ephemeral
|
|
139
|
+
* state that survived, in the same way as the framework providers.
|
|
140
|
+
*/
|
|
141
|
+
treeGeneration = void 0;
|
|
142
|
+
/**
|
|
143
|
+
* The incremental renderer of the live tree, and the `root` and `elements` of the
|
|
144
|
+
* spec that it renders. An emission with the same viewKey and the same two
|
|
145
|
+
* references moved the /context projection alone. An emission with the same
|
|
146
|
+
* viewKey and a new `elements` object is a new spec of the same view — a composed
|
|
147
|
+
* view whose page changed, or a streamed chunk — and the renderer takes it in place
|
|
148
|
+
* through `setSpec`.
|
|
149
|
+
*/
|
|
150
|
+
incremental = null;
|
|
151
|
+
lastRoot = void 0;
|
|
152
|
+
lastElements = void 0;
|
|
153
|
+
/** The fallback element, built at the first moment that the renderer shows it. */
|
|
154
|
+
fallbackElement = null;
|
|
155
|
+
/**
|
|
156
|
+
* The view session of the connection, from @xmachines/play-view. It resolves the store
|
|
157
|
+
* of each view by the rules of the lifecycle — it seeds the store again on a change of
|
|
158
|
+
* the viewKey, and it refreshes /context in place in every other case — and it hands
|
|
159
|
+
* this renderer one snapshot for each attempt. Its generation decides whether the live
|
|
160
|
+
* tree stays. `connect()` makes one, and `disconnect()` drops it with its store.
|
|
161
|
+
*/
|
|
162
|
+
session = null;
|
|
163
|
+
/**
|
|
164
|
+
* `disconnect()` sets this field to `false` before it calls `storeUnsub()`.
|
|
165
|
+
* The `rerender` closure reads the flag at its start. Therefore a synchronous
|
|
166
|
+
* callback from the `StateStore` implementation inside its own `unsubscribe()`
|
|
167
|
+
* call, which is rare but correct, changes no detached DOM tree.
|
|
168
|
+
*/
|
|
169
|
+
alive = true;
|
|
170
|
+
/**
|
|
171
|
+
* The depth of the synchronous calls into the inner renderer: `render()`, `setSpec()`
|
|
172
|
+
* and `update()`. A failure of the root that the inner renderer reports inside such a
|
|
173
|
+
* call waits for the end of the call. See {@link PlayRenderer.failRoot}.
|
|
174
|
+
*/
|
|
175
|
+
innerDepth = 0;
|
|
176
|
+
/**
|
|
177
|
+
* The failure of the root that the inner renderer reported, until the renderer
|
|
178
|
+
* contains it, and the inner renderer that reported it.
|
|
179
|
+
*/
|
|
180
|
+
rootFailure = null;
|
|
181
|
+
/**
|
|
182
|
+
* The report guard, from @xmachines/play-view.
|
|
183
|
+
*
|
|
184
|
+
* It blocks a reset that the host calls from INSIDE the report, and every reset once
|
|
185
|
+
* the renderer is disconnected. The five renderers share the one implementation.
|
|
186
|
+
*
|
|
187
|
+
* `alive` cannot answer the second half. A contained failure sets that flag to `false`
|
|
188
|
+
* and the next emission sets it back, because it guards the callback of the store
|
|
189
|
+
* subscription — a reset that a host keeps outlives both.
|
|
190
|
+
*/
|
|
191
|
+
guard = createReportGuard(GUARD_MESSAGES);
|
|
192
|
+
/**
|
|
193
|
+
* The retry that every report of THIS connection hands the host.
|
|
194
|
+
*
|
|
195
|
+
* `connect()` builds one, beside the guard that it belongs to. The four framework
|
|
196
|
+
* renderers hand the host one reset for the life of the provider, and this field holds
|
|
197
|
+
* the same rule for the life of the connection.
|
|
198
|
+
*/
|
|
199
|
+
retry = this.retryOf(this.guard);
|
|
200
|
+
/**
|
|
201
|
+
* @param container - The `HTMLElement` to render into. Each view transition clears it and fills it again.
|
|
202
|
+
* @param actor - The actor with the `currentView` atom. It must implement `Viewable`.
|
|
203
|
+
* @param registry - The map of the component renderers, usually `registryResult.registry` from `defineRegistry`.
|
|
204
|
+
* @param options - The configuration. See {@link PlayDomOptions}:
|
|
205
|
+
* - `registryResult` — it connects `setState` and `state` of the xstate store for you.
|
|
206
|
+
* - `store` — an external `StateStore`, which is the controlled mode. It replaces the values of `spec.state`.
|
|
207
|
+
* - `loading` — the flag of the streaming mode. It stops each warning about an absent child.
|
|
208
|
+
* - `functions` — the named compute functions of a `$computed` prop expression.
|
|
209
|
+
* - `directives` — your own dynamic values with a `$` prefix. The renderer resolves them with the props.
|
|
210
|
+
* - `validationFunctions` — your own check functions. They are available at `ctx.ctx.validationFunctions`.
|
|
211
|
+
* - `navigate` — the navigation callback. The renderer calls it for `onSuccess: { navigate: "..." }`.
|
|
212
|
+
* - `onRenderError` — the `(error, name, info)` handler of each failure that the inner renderer reports. It stops the `[json-render]` console line.
|
|
213
|
+
* - `onError` — the `(error, reset)` handler of a failure of the view: a failure of the root, or of a complete rebuild. The renderer contains such a failure always, and this option says where the report goes.
|
|
214
|
+
* - `fallback` — the element to show for every null view, and after a contained failure.
|
|
215
|
+
*/
|
|
216
|
+
constructor(container, actor, registry, options = {}) {
|
|
217
|
+
this.container = container;
|
|
218
|
+
this.actor = actor;
|
|
219
|
+
this.registry = registry;
|
|
220
|
+
this.options = options;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Starts the watch of actor.currentView, and renders into the container.
|
|
224
|
+
* It renders the first view synchronously, then it subscribes to the atom changes.
|
|
225
|
+
*
|
|
226
|
+
* A `connect()` call on a connected renderer, where a `disconnect()` call did not
|
|
227
|
+
* follow the previous `connect()`, installs a second subscription of the view.
|
|
228
|
+
* Each view change then renders two times. Therefore this method disconnects the
|
|
229
|
+
* renderer first.
|
|
230
|
+
*/
|
|
231
|
+
connect() {
|
|
232
|
+
if (this.connected) this.disconnect();
|
|
233
|
+
this.connected = true;
|
|
234
|
+
const guard = createReportGuard(GUARD_MESSAGES);
|
|
235
|
+
this.guard = guard;
|
|
236
|
+
this.retry = this.retryOf(guard);
|
|
237
|
+
const session = createViewSession(this.actor, this.options.store, { createStore: (seed) => xstateStoreStateStore({ atom: createAtom(seed) }) });
|
|
238
|
+
this.session = session;
|
|
239
|
+
const release = session.subscribe(() => {
|
|
240
|
+
this.render(session.getSnapshot());
|
|
241
|
+
});
|
|
242
|
+
this.unwatch = release;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Stops the watch and clears the container.
|
|
246
|
+
*/
|
|
247
|
+
disconnect() {
|
|
248
|
+
this.connected = false;
|
|
249
|
+
this.guard.dispose();
|
|
250
|
+
releaseQuietly(this.unwatch, reportReleaseFailure);
|
|
251
|
+
this.unwatch = null;
|
|
252
|
+
this.alive = false;
|
|
253
|
+
this.releaseTree();
|
|
254
|
+
this.container.replaceChildren();
|
|
255
|
+
this.forgetTree();
|
|
256
|
+
this.session?.dispose();
|
|
257
|
+
this.session = null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Releases the store subscription and the watch subscriptions of the tree.
|
|
261
|
+
*
|
|
262
|
+
* It captures both and clears both BEFORE a release. Some store implementations fire a
|
|
263
|
+
* synchronous callback during their unsubscribe(). Without this order, that callback
|
|
264
|
+
* sees a live storeUnsubscribe, and it enters the rerender path again with an old view
|
|
265
|
+
* and an old store, or it overwrites watchCleanups before the loop below reaches it.
|
|
266
|
+
*
|
|
267
|
+
* See reportReleaseFailure: a throw of one release must skip none of the work below it.
|
|
268
|
+
* This is code of a caller, and it also runs on the path of a view that WORKS.
|
|
269
|
+
*/
|
|
270
|
+
releaseTree() {
|
|
271
|
+
const storeUnsub = this.storeUnsubscribe;
|
|
272
|
+
const watchCleanups = [...this.watchCleanups];
|
|
273
|
+
const incremental = this.incremental;
|
|
274
|
+
this.storeUnsubscribe = null;
|
|
275
|
+
this.watchCleanups = /* @__PURE__ */ new Set();
|
|
276
|
+
this.rootFailure = null;
|
|
277
|
+
releaseQuietly(storeUnsub, reportReleaseFailure);
|
|
278
|
+
if (incremental !== null) releaseQuietly(() => incremental.dispose(), reportReleaseFailure);
|
|
279
|
+
for (const cleanup of watchCleanups) releaseQuietly(cleanup, reportReleaseFailure);
|
|
280
|
+
}
|
|
281
|
+
/** Forgets the tree on the screen, so that the next emission builds it again. */
|
|
282
|
+
forgetTree() {
|
|
283
|
+
this.treeGeneration = void 0;
|
|
284
|
+
this.incremental = null;
|
|
285
|
+
this.lastRoot = void 0;
|
|
286
|
+
this.lastElements = void 0;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The number of live watch-subscription cleanup functions now.
|
|
290
|
+
*
|
|
291
|
+
* @internal This accessor is for a test only. A test asserts with it that the
|
|
292
|
+
* collection of the watch cleanups holds the live registrations, and that no mount
|
|
293
|
+
* leaks. It is not part of the public API, and it can change without a notice.
|
|
294
|
+
*/
|
|
295
|
+
get watchCleanupCount() {
|
|
296
|
+
return this.watchCleanups.size;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Brings the screen in line with one snapshot of the view session.
|
|
300
|
+
*
|
|
301
|
+
* The session resolved the store already: it refreshed /context in place, or it seeded
|
|
302
|
+
* a new store, or it recorded the failure of `spec.state`. This method decides the
|
|
303
|
+
* tree alone.
|
|
304
|
+
*/
|
|
305
|
+
render(snapshot) {
|
|
306
|
+
const { view } = snapshot;
|
|
307
|
+
if (snapshot.failure) {
|
|
308
|
+
this.failView(snapshot.failure.error);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (view !== null && this.incremental !== null && this.storeUnsubscribe !== null) {
|
|
312
|
+
if (snapshot.generation === this.treeGeneration) {
|
|
313
|
+
const incremental = this.incremental;
|
|
314
|
+
try {
|
|
315
|
+
if (view.root !== this.lastRoot || view.elements !== this.lastElements) {
|
|
316
|
+
this.lastRoot = view.root;
|
|
317
|
+
this.lastElements = view.elements;
|
|
318
|
+
this.callInner(incremental, () => incremental.setSpec(view));
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
this.failView(error);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this.clearTree();
|
|
327
|
+
if (view === null || snapshot.store === null) {
|
|
328
|
+
this.showFallback();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
this.renderView(view, snapshot.store, snapshot.generation);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
this.failView(error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Takes the tree of the previous view off the screen, before a rebuild.
|
|
339
|
+
*
|
|
340
|
+
* The watch subscriptions of the previous view must go too. Without this step, they
|
|
341
|
+
* stay active between a view transition and the first store update of the new view.
|
|
342
|
+
* They then hold a reference to a DOM element that is gone, and the garbage collector
|
|
343
|
+
* cannot take it.
|
|
344
|
+
*/
|
|
345
|
+
clearTree() {
|
|
346
|
+
this.alive = true;
|
|
347
|
+
this.releaseTree();
|
|
348
|
+
this.forgetTree();
|
|
349
|
+
this.container.replaceChildren();
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The end of a render that failed, in ONE place.
|
|
353
|
+
*
|
|
354
|
+
* Three paths render — the fast path of a slice, the complete rebuild, and the write
|
|
355
|
+
* of the store — and each one ends here: through a throw, or through a failure of the
|
|
356
|
+
* root that the inner renderer reports (see {@link PlayRenderer.failRoot}). The ORDER
|
|
357
|
+
* is load-bearing: the reset takes the registrations of the dead tree down BEFORE the
|
|
358
|
+
* report, so a handler of the host that renders again meets a renderer that holds
|
|
359
|
+
* nothing of the failed attempt. A second copy of the pair is a second chance to put
|
|
360
|
+
* the two in the other order.
|
|
361
|
+
*
|
|
362
|
+
* @param error - The failure that the renderer contains.
|
|
363
|
+
*/
|
|
364
|
+
failView(error) {
|
|
365
|
+
this.resetFailedRebuild();
|
|
366
|
+
this.contain(error);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Gives a failure that the inner renderer reports to the handler of the host, and
|
|
370
|
+
* contains a failure of the ROOT as a failure of the view.
|
|
371
|
+
*
|
|
372
|
+
* The inner renderer reports each failure one time, with `(error, name, info)`. This
|
|
373
|
+
* method gives each one to `forwardRenderError` of @xmachines/play-view, which holds
|
|
374
|
+
* the rule of the five renderers: the `onRenderError` of the host, or the handler of the
|
|
375
|
+
* registry, or the one `[json-render]` console line.
|
|
376
|
+
*
|
|
377
|
+
* A failure of the root — its component, a prop, or its `visible` condition — leaves
|
|
378
|
+
* no view on the screen. The inner renderer throws it to no caller, so the renderer
|
|
379
|
+
* contains it here, as it contains a complete rebuild that throws: it shows the
|
|
380
|
+
* fallback, and it reports the failure to `onError` one time. A handler of the host
|
|
381
|
+
* also gets it, with `info.kind` `"root"`. Without such a handler, the method writes
|
|
382
|
+
* no `[json-render]` line for it: the containment reports it to `onError`, or to its
|
|
383
|
+
* own console line, and a second line would report one failure two times.
|
|
384
|
+
*
|
|
385
|
+
* @param owner - The inner renderer that this handler belongs to, or `null` before it exists.
|
|
386
|
+
*/
|
|
387
|
+
reportInner(owner, error, name, info) {
|
|
388
|
+
if (forwardRenderError(this.options.onRenderError, this.registry, error, name, info) && owner !== null) this.failRoot(owner, error);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Contains a failure of the root that the inner renderer reported.
|
|
392
|
+
*
|
|
393
|
+
* The report comes in the middle of the work of the inner renderer, which writes the
|
|
394
|
+
* container after it. The containment therefore waits for the end of that work. Inside
|
|
395
|
+
* a synchronous call, {@link PlayRenderer.callInner} contains the failure when the call
|
|
396
|
+
* returns. At the commit point of a store write, which no call of this class holds, a
|
|
397
|
+
* microtask contains it.
|
|
398
|
+
*
|
|
399
|
+
* @param owner - The inner renderer that reported the failure.
|
|
400
|
+
* @param error - The failure of the root.
|
|
401
|
+
*/
|
|
402
|
+
failRoot(owner, error) {
|
|
403
|
+
if (this.incremental !== owner || this.rootFailure !== null) return;
|
|
404
|
+
this.rootFailure = {
|
|
405
|
+
error,
|
|
406
|
+
owner
|
|
407
|
+
};
|
|
408
|
+
if (this.innerDepth > 0) return;
|
|
409
|
+
queueMicrotask(() => this.settleRoot());
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Hands the failure of the root that waits to the session, when it belongs to the
|
|
413
|
+
* live tree.
|
|
414
|
+
*
|
|
415
|
+
* The four framework providers take the same path: `failView` of the session records
|
|
416
|
+
* the failure in a new snapshot, and it gives the next attempt a new generation. The
|
|
417
|
+
* session then notifies this renderer, and `render()` contains the failed snapshot
|
|
418
|
+
* with {@link PlayRenderer.failView}: it clears the tree, shows the fallback, and
|
|
419
|
+
* reports to `onError` one time. The session has no `onFailure` here, so it reports
|
|
420
|
+
* nothing of its own.
|
|
421
|
+
*/
|
|
422
|
+
settleRoot() {
|
|
423
|
+
const failure = this.rootFailure;
|
|
424
|
+
this.rootFailure = null;
|
|
425
|
+
if (failure === null || this.incremental !== failure.owner) return;
|
|
426
|
+
this.session?.failView(failure.error);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Runs one synchronous call into the inner renderer, and then contains a failure of the
|
|
430
|
+
* root that the call reported.
|
|
431
|
+
*
|
|
432
|
+
* @param owner - The inner renderer of the call.
|
|
433
|
+
* @param call - The call: `render()`, `setSpec()` or `update()`.
|
|
434
|
+
*/
|
|
435
|
+
callInner(owner, call) {
|
|
436
|
+
this.innerDepth += 1;
|
|
437
|
+
try {
|
|
438
|
+
call();
|
|
439
|
+
} finally {
|
|
440
|
+
this.innerDepth -= 1;
|
|
441
|
+
}
|
|
442
|
+
if (this.innerDepth === 0 && this.rootFailure?.owner === owner) this.settleRoot();
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Clears the screen of the failed view, shows the fallback, and reports the failure.
|
|
446
|
+
*
|
|
447
|
+
* The renderer contains such a failure ALWAYS. No option turns the containment on,
|
|
448
|
+
* and none turns it off: `onError` says where the report goes, and `fallback` says
|
|
449
|
+
* what the empty container shows. The four framework renderers hold the same rule,
|
|
450
|
+
* because an error boundary of a framework is not an option that a caller turns off.
|
|
451
|
+
* A host that must escalate a failure raises it from a task of its own —
|
|
452
|
+
* `rethrowLater(error)` of `@xmachines/core/utils` — because the guard contains a handler
|
|
453
|
+
* that throws, exactly as the four framework renderers do.
|
|
454
|
+
*
|
|
455
|
+
* The container is cleared FIRST. The fast path of a slice holds the live tree of the
|
|
456
|
+
* previous emission, and that tree is dead as soon as the refresh throws, so the
|
|
457
|
+
* renderer must not keep it on the screen. A clear container also lets the fallback
|
|
458
|
+
* stand alone.
|
|
459
|
+
*
|
|
460
|
+
* The SCREEN comes first, and the report second: a handler that throws — a reporter
|
|
461
|
+
* that fails, or one that re-raises to escalate — must not defeat the containment
|
|
462
|
+
* that it was given to observe. With the order reversed the container stays empty AND
|
|
463
|
+
* the error escapes, which is both outcomes that the containment exists to prevent.
|
|
464
|
+
*/
|
|
465
|
+
contain(error) {
|
|
466
|
+
this.container.replaceChildren();
|
|
467
|
+
this.showFallback();
|
|
468
|
+
const { onError } = this.options;
|
|
469
|
+
const guard = this.guard;
|
|
470
|
+
const retry = this.retry;
|
|
471
|
+
guard.report(error, onError && (() => onError(error, retry)));
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Builds the retry that one report hands the host.
|
|
475
|
+
*
|
|
476
|
+
* The read of the atom happens at the moment of the CALL: a retry that held the view
|
|
477
|
+
* that failed would take a host off the healthy screen and back onto the one that
|
|
478
|
+
* threw.
|
|
479
|
+
*
|
|
480
|
+
* The retry belongs to the guard of ITS connection, and it asks that guard alone. A
|
|
481
|
+
* host may KEEP the callback — a "Retry" button of a toast outlives the route that
|
|
482
|
+
* opened it — and `disconnect()` disposes the guard that was live then. `connect()`
|
|
483
|
+
* disconnects a connected renderer first, so every guard that leaves the field is a
|
|
484
|
+
* disposed guard, and a retry of an older connection stays dead. The rule of the prop
|
|
485
|
+
* is one rule: a reset after `disconnect()` does nothing.
|
|
486
|
+
*
|
|
487
|
+
* @param guard - The guard of the connection that this retry belongs to.
|
|
488
|
+
*/
|
|
489
|
+
retryOf(guard) {
|
|
490
|
+
return () => {
|
|
491
|
+
if (guard.blocked()) return;
|
|
492
|
+
this.session?.retry();
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Appends the fallback element to the container, which the caller cleared already.
|
|
497
|
+
* The renderer calls it for a null view, and after a failure of a rebuild that it
|
|
498
|
+
* contains. Both callers clear the container first. `appendChild` moves the node.
|
|
499
|
+
* Therefore a second call adds no copy.
|
|
500
|
+
*/
|
|
501
|
+
showFallback() {
|
|
502
|
+
const fallback = this.options.fallback;
|
|
503
|
+
if (fallback == null) return;
|
|
504
|
+
try {
|
|
505
|
+
this.fallbackElement ??= typeof fallback === "function" ? fallback() : fallback;
|
|
506
|
+
this.container.appendChild(this.fallbackElement);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error("[@xmachines/play-dom] the fallback of PlayRenderer failed to build. The container stays empty.", error);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* A rebuild that fails must not leave the guard of the fast path armed on a dead
|
|
513
|
+
* tree, because the container is empty already. Therefore this code resets the view
|
|
514
|
+
* identity. The next emission, also one with the same viewKey, then makes a
|
|
515
|
+
* complete render again. It does not patch a store that nothing shows.
|
|
516
|
+
*
|
|
517
|
+
* It releases the watch subscriptions of the dead tree too. A complete rebuild
|
|
518
|
+
* releases them before it builds again, and disconnect() releases them at the end of
|
|
519
|
+
* the life of the renderer — but a failure takes the DOM down HERE, between those two
|
|
520
|
+
* moments. Without this step one update of the store of the host runs every watch
|
|
521
|
+
* action of a tree that nothing shows, against detached elements, with `send` still
|
|
522
|
+
* bound to the actor. The fast path of a slice leaves a live tree of its own, and it
|
|
523
|
+
* reaches this code only when it fails.
|
|
524
|
+
*/
|
|
525
|
+
resetFailedRebuild() {
|
|
526
|
+
this.alive = false;
|
|
527
|
+
this.releaseTree();
|
|
528
|
+
this.forgetTree();
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* The tail of a complete rebuild, which can throw. See the catch block of render().
|
|
532
|
+
*
|
|
533
|
+
* @param view - The view to render.
|
|
534
|
+
* @param guardedStore - The store of the view, from the session. Everything that the
|
|
535
|
+
* spec can write through, which includes $bindState, setState, and a chained set,
|
|
536
|
+
* receives the GUARDED store: /context is read-only to the spec, because the machine
|
|
537
|
+
* context changes through an event only.
|
|
538
|
+
* @param generation - The generation of that store, which the tree records.
|
|
539
|
+
*/
|
|
540
|
+
renderView(view, guardedStore, generation) {
|
|
541
|
+
const send = this.actor.send.bind(this.actor);
|
|
542
|
+
const handlers = this.options.registryResult ? bindStoreHandlers(this.options.registryResult, () => guardedStore) : {};
|
|
543
|
+
const loading = this.options.loading;
|
|
544
|
+
const functions = this.options.functions;
|
|
545
|
+
const directives = this.options.directives;
|
|
546
|
+
const validationFunctions = this.options.validationFunctions;
|
|
547
|
+
const navigate = this.options.navigate;
|
|
548
|
+
const onConfirm = this.options.onConfirm;
|
|
549
|
+
const confirmDialog = this.options.confirmDialog;
|
|
550
|
+
let owner = null;
|
|
551
|
+
const onRenderError = (error, name, info) => {
|
|
552
|
+
this.reportInner(owner, error, name, info);
|
|
553
|
+
};
|
|
554
|
+
const renderOptions = {
|
|
555
|
+
send,
|
|
556
|
+
handlers,
|
|
557
|
+
onRenderError,
|
|
558
|
+
...functions !== void 0 ? { functions } : {},
|
|
559
|
+
...directives !== void 0 ? { directives } : {},
|
|
560
|
+
...loading !== void 0 ? { loading } : {},
|
|
561
|
+
onWatchSetup: (cleanup) => {
|
|
562
|
+
const entry = () => {
|
|
563
|
+
this.watchCleanups.delete(entry);
|
|
564
|
+
cleanup();
|
|
565
|
+
};
|
|
566
|
+
this.watchCleanups.add(entry);
|
|
567
|
+
return entry;
|
|
568
|
+
},
|
|
569
|
+
...validationFunctions !== void 0 ? { validationFunctions } : {},
|
|
570
|
+
...navigate !== void 0 ? { navigate } : {},
|
|
571
|
+
...onConfirm !== void 0 ? { onConfirm } : {},
|
|
572
|
+
...confirmDialog !== void 0 ? { confirmDialog } : {}
|
|
573
|
+
};
|
|
574
|
+
const incremental = createIncrementalRenderer(view, guardedStore, this.registry, this.container, renderOptions);
|
|
575
|
+
owner = incremental;
|
|
576
|
+
this.incremental = incremental;
|
|
577
|
+
this.treeGeneration = generation;
|
|
578
|
+
this.lastRoot = view.root;
|
|
579
|
+
this.lastElements = view.elements;
|
|
580
|
+
this.storeUnsubscribe = guardedStore.subscribe(() => {
|
|
581
|
+
if (!this.alive) return;
|
|
582
|
+
try {
|
|
583
|
+
this.callInner(incremental, () => incremental.update());
|
|
584
|
+
} catch (error) {
|
|
585
|
+
this.failView(error);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
this.callInner(incremental, () => incremental.render());
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
//#endregion
|
|
592
|
+
export { PlayRenderer };
|
|
593
|
+
|
|
689
594
|
//# sourceMappingURL=PlayRenderer.js.map
|