effectweb 0.3.0 → 0.4.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 +35 -4
- package/dist/AsyncContent.d.ts +1 -1
- package/dist/AsyncContent.js +18 -35
- package/dist/boundary.d.ts +11 -0
- package/dist/boundary.js +66 -0
- package/dist/cache-internals.d.ts +4 -0
- package/dist/cache.d.ts +21 -5
- package/dist/cache.js +178 -25
- package/dist/collection.d.ts +1 -1
- package/dist/collection.js +1 -3
- package/dist/component.d.ts +3 -0
- package/dist/component.js +73 -10
- package/dist/dom.d.ts +35 -24
- package/dist/dom.js +177 -128
- package/dist/effectEvent.d.ts +5 -4
- package/dist/effectEvent.js +29 -38
- package/dist/form.d.ts +2 -1
- package/dist/http.d.ts +2 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/infinite-query.d.ts +42 -0
- package/dist/infinite-query.js +150 -0
- package/dist/jsx-runtime.d.ts +6 -0
- package/dist/jsx-runtime.js +14 -1
- package/dist/jsx.d.ts +2 -1
- package/dist/keyed-tasks.d.ts +30 -0
- package/dist/keyed-tasks.js +102 -0
- package/dist/lazy.d.ts +2 -1
- package/dist/lazy.js +20 -7
- package/dist/load.d.ts +1 -1
- package/dist/load.js +1 -1
- package/dist/mount.d.ts +13 -2
- package/dist/mount.js +82 -21
- package/dist/owner.d.ts +9 -5
- package/dist/owner.js +27 -9
- package/dist/pages.d.ts +16 -15
- package/dist/pages.js +8 -5
- package/dist/program.d.ts +14 -2
- package/dist/program.js +96 -36
- package/dist/query-internals.d.ts +5 -3
- package/dist/query.d.ts +7 -1
- package/dist/query.js +7 -3
- package/dist/resource.js +3 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +3 -1
- package/dist/session.js +23 -7
- package/dist/settlement.d.ts +8 -0
- package/dist/settlement.js +32 -0
- package/dist/sharing.js +16 -13
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +31 -25
- package/dist/task.d.ts +1 -1
- package/dist/task.js +3 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +3 -1
- package/dist/testing.d.ts +5 -4
- package/dist/testing.js +4 -2
- package/package.json +9 -1
package/dist/dom.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import attributeData from './dom-attributes.json' with { type: 'json' };
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
2
3
|
import { validateIdentities } from './collection.js';
|
|
3
4
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
4
5
|
import { eventEffects } from './effectEvent.js';
|
|
5
6
|
import { traceBinding } from './diagnostics.js';
|
|
6
|
-
import { shareData } from './sharing.js';
|
|
7
7
|
import { jsxComponent } from './jsx.js';
|
|
8
|
-
import {
|
|
8
|
+
import { Settlement } from './settlement.js';
|
|
9
|
+
import { prepareMount } from './mount.js';
|
|
10
|
+
const bindingLocation = (source) => typeof source === 'function' ? source() : source;
|
|
9
11
|
const equal = (a, b) => a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
|
|
10
12
|
// A model publication reconciles DOM first and controlled dependent properties second.
|
|
11
13
|
// Nested child scopes join the same synchronous commit; there is no reactive graph.
|
|
@@ -36,43 +38,27 @@ export class Scope {
|
|
|
36
38
|
value;
|
|
37
39
|
send;
|
|
38
40
|
report;
|
|
41
|
+
settlement;
|
|
39
42
|
jobs = [];
|
|
40
43
|
cleanups = [];
|
|
41
44
|
disposed = false;
|
|
42
|
-
|
|
43
|
-
constructor(value, send, report = reportError) {
|
|
45
|
+
constructor(value, send, report = reportError, settlement = new Settlement()) {
|
|
44
46
|
this.value = value;
|
|
45
47
|
this.send = send;
|
|
46
48
|
this.report = report;
|
|
47
|
-
|
|
48
|
-
derive(dependencies, compute, source) {
|
|
49
|
-
let previous;
|
|
50
|
-
let value;
|
|
51
|
-
let revision = -1;
|
|
52
|
-
return () => {
|
|
53
|
-
if (revision === this.revision)
|
|
54
|
-
return value;
|
|
55
|
-
const next = dependencies();
|
|
56
|
-
if (!previous || !equal(previous, next)) {
|
|
57
|
-
value = previous ? shareData(value, compute()) : compute();
|
|
58
|
-
traceBinding(source, previous, next, 'derive');
|
|
59
|
-
previous = next;
|
|
60
|
-
}
|
|
61
|
-
revision = this.revision;
|
|
62
|
-
return value;
|
|
63
|
-
};
|
|
49
|
+
this.settlement = settlement;
|
|
64
50
|
}
|
|
65
51
|
watch(dependencies, apply, source) {
|
|
66
52
|
let previous = dependencies();
|
|
67
53
|
apply();
|
|
68
|
-
traceBinding(source, undefined, previous, 'binding');
|
|
54
|
+
traceBinding(bindingLocation(source), undefined, previous, 'binding');
|
|
69
55
|
if (!previous.length)
|
|
70
56
|
return;
|
|
71
57
|
const run = () => {
|
|
72
58
|
const next = dependencies();
|
|
73
59
|
if (!equal(previous, next)) {
|
|
74
60
|
apply();
|
|
75
|
-
traceBinding(source, previous, next, 'binding');
|
|
61
|
+
traceBinding(bindingLocation(source), previous, next, 'binding');
|
|
76
62
|
previous = next;
|
|
77
63
|
}
|
|
78
64
|
};
|
|
@@ -83,7 +69,6 @@ export class Scope {
|
|
|
83
69
|
return;
|
|
84
70
|
commitDom(() => {
|
|
85
71
|
this.value = value;
|
|
86
|
-
this.revision++;
|
|
87
72
|
for (const job of this.jobs) {
|
|
88
73
|
if (this.disposed)
|
|
89
74
|
break;
|
|
@@ -104,18 +89,18 @@ export class Scope {
|
|
|
104
89
|
runAll(this.cleanups.splice(0).reverse(), this.report);
|
|
105
90
|
}
|
|
106
91
|
}
|
|
107
|
-
/**
|
|
108
|
-
export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(
|
|
109
|
-
|
|
92
|
+
/** Mount a view with an explicit model and message dispatcher. */
|
|
93
|
+
export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(props) {
|
|
94
|
+
return new SlotPlacement(viewContent(props.view), { model: props.model, send: props.send });
|
|
110
95
|
}, { [jsxComponent]: true });
|
|
111
96
|
/** Untyped callers get an actionable failure instead of silently losing child messages. */
|
|
112
97
|
export function unboundSend(_message) {
|
|
113
98
|
throw new Error('This view needs a dispatcher. Mount it through ViewBinding with model and send.');
|
|
114
99
|
}
|
|
115
100
|
const contentBrand = Symbol('compiled content');
|
|
116
|
-
|
|
117
|
-
export function slot(
|
|
118
|
-
|
|
101
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
102
|
+
export function slot(render) {
|
|
103
|
+
return render;
|
|
119
104
|
}
|
|
120
105
|
// Placement values are opaque to structural sharing, which only traverses plain data.
|
|
121
106
|
class SlotPlacement {
|
|
@@ -132,60 +117,83 @@ function isContent(value) {
|
|
|
132
117
|
(typeof value === 'object' || typeof value === 'function') &&
|
|
133
118
|
contentBrand in value);
|
|
134
119
|
}
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
owner.cleanups.push(() => {
|
|
143
|
-
for (const scope of placements)
|
|
144
|
-
scope.dispose();
|
|
145
|
-
placements.clear();
|
|
120
|
+
/** Evaluate ordinary synchronous JavaScript for each immutable model publication. */
|
|
121
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
122
|
+
export function view(render) {
|
|
123
|
+
return compiled((scope, parent, before) => {
|
|
124
|
+
const read = () => render(scope.value, scope.send);
|
|
125
|
+
text(scope, parent, before, () => [scope.value], read);
|
|
146
126
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
127
|
+
}
|
|
128
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
129
|
+
export function compiled(build) {
|
|
130
|
+
const definition = Object.assign((props) => new SlotPlacement(viewContent(definition), { model: props, send: unboundSend }), { build, [jsxComponent]: true });
|
|
131
|
+
return definition;
|
|
132
|
+
}
|
|
133
|
+
const viewDefinitions = new WeakMap();
|
|
134
|
+
function viewContent(definition) {
|
|
135
|
+
let content = viewDefinitions.get(definition);
|
|
136
|
+
if (!content) {
|
|
137
|
+
content = contentDefinition((scope, parent, before) => {
|
|
138
|
+
const childScope = new Scope(scope.value.model, (message) => scope.value.send(message), scope.report, scope.settlement);
|
|
139
|
+
scope.cleanups.push(() => childScope.dispose());
|
|
140
|
+
definition.build(childScope, parent, before);
|
|
141
|
+
scope.jobs.push(() => childScope.set(scope.value.model));
|
|
142
|
+
});
|
|
143
|
+
viewDefinitions.set(definition, content);
|
|
144
|
+
}
|
|
145
|
+
return content;
|
|
146
|
+
}
|
|
147
|
+
function contentDefinition(build) {
|
|
148
|
+
return {
|
|
149
|
+
mount(parent, before, value, report, settlement) {
|
|
150
|
+
const scope = new Scope(value, unboundSend, report, settlement);
|
|
152
151
|
const fragment = buildFragment(parent);
|
|
153
152
|
const range = markers(fragment, null);
|
|
154
153
|
scope.cleanups.push(() => remove(range.start, range.end));
|
|
155
154
|
try {
|
|
156
155
|
build(scope, fragment, range.end);
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
if (!scope.disposed)
|
|
157
|
+
parent.insertBefore(fragment, before);
|
|
159
158
|
}
|
|
160
159
|
catch (error) {
|
|
161
160
|
scope.dispose();
|
|
162
161
|
throw error;
|
|
163
162
|
}
|
|
164
|
-
return {
|
|
165
|
-
set(value) {
|
|
166
|
-
if (!Object.is(scope.value, value))
|
|
167
|
-
scope.set(value);
|
|
168
|
-
},
|
|
169
|
-
dispose() {
|
|
170
|
-
placements.delete(scope);
|
|
171
|
-
scope.dispose();
|
|
172
|
-
},
|
|
173
|
-
};
|
|
163
|
+
return { set: (value) => scope.set(value), dispose: () => scope.dispose() };
|
|
174
164
|
},
|
|
175
165
|
};
|
|
176
|
-
return Object.assign((value) => new SlotPlacement(definition, value), {
|
|
177
|
-
[contentBrand]: true,
|
|
178
|
-
definition,
|
|
179
|
-
});
|
|
180
166
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
167
|
+
const markupDefinitions = new Map();
|
|
168
|
+
/** JSX factories preserve host identity by element type; source locations are metadata only. */
|
|
169
|
+
export function markup(tag, sources) {
|
|
170
|
+
let definition = markupDefinitions.get(tag);
|
|
171
|
+
if (!definition) {
|
|
172
|
+
definition = contentDefinition((scope, parent, before) => {
|
|
173
|
+
const node = element(parent, before, tag);
|
|
174
|
+
bindAttributes(scope, node, () => [scope.value.props], () => {
|
|
175
|
+
const { children: _children, ...attributes } = scope.value.props;
|
|
176
|
+
return attributes;
|
|
177
|
+
}, () => scope.value.sources);
|
|
178
|
+
text(scope, node, null, () => [scope.value.props.children], () => scope.value.props.children, () => scope.value.sources?.children);
|
|
179
|
+
});
|
|
180
|
+
markupDefinitions.set(tag, definition);
|
|
181
|
+
}
|
|
182
|
+
return (props) => new SlotPlacement(definition, { props, sources });
|
|
183
|
+
}
|
|
184
|
+
export function renderComponent(component, props) {
|
|
185
|
+
return component(props);
|
|
184
186
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
const listDefinition = contentDefinition((scope, parent, before) => {
|
|
188
|
+
each(scope, parent, before, () => scope.value.rows, () => [scope.value.render], true, (row, parent, before) => {
|
|
189
|
+
// The explicit list operation owns callback evaluation and keyed row lifetimes.
|
|
190
|
+
const update = () => scope.value.render(row.value[0], row.value[1]);
|
|
191
|
+
text(row, parent, before, () => [row.value, scope.value.render], update);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
/** Preserve collection identity (or array reference identity) while rendering rows. */
|
|
195
|
+
export function list(rows, render) {
|
|
196
|
+
return new SlotPlacement(listDefinition, { rows, render });
|
|
189
197
|
}
|
|
190
198
|
/** Owned content in an element target, defaulting to the document body. */
|
|
191
199
|
export const Portal = /* @__PURE__ */ compiled((scope) => {
|
|
@@ -257,22 +265,39 @@ export function mountView(parent, definition, source, options = {}) {
|
|
|
257
265
|
const { start, end } = markers(parent, null);
|
|
258
266
|
const scope = new Scope(source.model(), source.send, options.onError);
|
|
259
267
|
let unsubscribe = () => { };
|
|
268
|
+
let ready = false;
|
|
269
|
+
let latest = scope.value;
|
|
260
270
|
try {
|
|
271
|
+
unsubscribe = source.subscribe((model) => {
|
|
272
|
+
latest = model;
|
|
273
|
+
if (ready)
|
|
274
|
+
scope.set(model);
|
|
275
|
+
});
|
|
261
276
|
const fragment = buildFragment(parent);
|
|
262
277
|
definition.build(scope, fragment, null);
|
|
263
278
|
parent.insertBefore(fragment, end);
|
|
264
|
-
|
|
279
|
+
ready = true;
|
|
280
|
+
if (!Object.is(latest, scope.value))
|
|
281
|
+
scope.set(latest);
|
|
265
282
|
}
|
|
266
283
|
catch (error) {
|
|
267
|
-
scope.dispose();
|
|
268
|
-
remove(start, end);
|
|
284
|
+
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
269
285
|
throw error;
|
|
270
286
|
}
|
|
271
|
-
|
|
287
|
+
const dispose = () => {
|
|
272
288
|
if (scope.disposed)
|
|
273
289
|
return;
|
|
274
290
|
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
275
291
|
};
|
|
292
|
+
return Object.assign(dispose, {
|
|
293
|
+
dispose,
|
|
294
|
+
close: () => {
|
|
295
|
+
return Effect.suspend(() => {
|
|
296
|
+
dispose();
|
|
297
|
+
return scope.settlement.wait();
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
});
|
|
276
301
|
});
|
|
277
302
|
}
|
|
278
303
|
export function element(parent, before, tag) {
|
|
@@ -372,7 +397,7 @@ export function bindAttribute(scope, element, name, dependencies, read, source)
|
|
|
372
397
|
let previous = dependencies();
|
|
373
398
|
let value = read();
|
|
374
399
|
attribute(element, name, value);
|
|
375
|
-
traceBinding(source, undefined, previous, 'binding');
|
|
400
|
+
traceBinding(bindingLocation(source), undefined, previous, 'binding');
|
|
376
401
|
if (!previous.length)
|
|
377
402
|
return;
|
|
378
403
|
// Keep the value cache in the dependency job itself, without an extra closure
|
|
@@ -386,7 +411,7 @@ export function bindAttribute(scope, element, name, dependencies, read, source)
|
|
|
386
411
|
attribute(element, name, nextValue);
|
|
387
412
|
value = nextValue;
|
|
388
413
|
}
|
|
389
|
-
traceBinding(source, previous, next, 'binding');
|
|
414
|
+
traceBinding(bindingLocation(source), previous, next, 'binding');
|
|
390
415
|
previous = next;
|
|
391
416
|
});
|
|
392
417
|
}
|
|
@@ -506,16 +531,20 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
506
531
|
let value = read();
|
|
507
532
|
if (Array.isArray(value)) {
|
|
508
533
|
validateContent(value);
|
|
509
|
-
value =
|
|
534
|
+
value = new SlotPlacement(arrayContent, value);
|
|
510
535
|
}
|
|
511
536
|
if (isContent(value)) {
|
|
512
|
-
node.data
|
|
537
|
+
if (node.data !== '')
|
|
538
|
+
node.data = '';
|
|
513
539
|
if (content?.definition === value.definition) {
|
|
514
540
|
content.mounted.set(value.value);
|
|
515
541
|
return;
|
|
516
542
|
}
|
|
517
|
-
content
|
|
543
|
+
const previous = content;
|
|
518
544
|
content = undefined;
|
|
545
|
+
previous?.mounted.dispose();
|
|
546
|
+
if (scope.disposed)
|
|
547
|
+
return;
|
|
519
548
|
if (start)
|
|
520
549
|
clear(start, node);
|
|
521
550
|
else {
|
|
@@ -523,10 +552,11 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
523
552
|
scope.cleanups.push(() => content?.mounted.dispose());
|
|
524
553
|
node.parentNode.insertBefore(start, node);
|
|
525
554
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
mounted
|
|
529
|
-
|
|
555
|
+
const mounted = value.definition.mount(node.parentNode, node, value.value, scope.report, scope.settlement);
|
|
556
|
+
if (scope.disposed)
|
|
557
|
+
mounted.dispose();
|
|
558
|
+
else
|
|
559
|
+
content = { definition: value.definition, mounted };
|
|
530
560
|
return;
|
|
531
561
|
}
|
|
532
562
|
if (value != null && typeof value === 'object')
|
|
@@ -557,32 +587,44 @@ function validateContent(value, active = new Set()) {
|
|
|
557
587
|
}
|
|
558
588
|
}
|
|
559
589
|
const arrayContent = {
|
|
560
|
-
mount: (_parent, before, value, report) => mountArray(report, before, value),
|
|
590
|
+
mount: (_parent, before, value, report, settlement) => mountArray(report, before, value, settlement),
|
|
561
591
|
};
|
|
562
592
|
/** Runtime content arrays are positional; domain lists retain the explicit collection contract. */
|
|
563
|
-
function mountArray(report, before, initial) {
|
|
593
|
+
function mountArray(report, before, initial, settlement) {
|
|
564
594
|
const cells = [];
|
|
595
|
+
let disposed = false;
|
|
565
596
|
const disposeCell = (cell) => {
|
|
566
597
|
cell.scope.dispose();
|
|
567
598
|
remove(cell.start, cell.end);
|
|
568
599
|
};
|
|
569
600
|
const set = (input) => {
|
|
601
|
+
if (disposed)
|
|
602
|
+
return;
|
|
570
603
|
const values = input;
|
|
571
604
|
validateContent(values);
|
|
572
|
-
while (cells.length > values.length)
|
|
605
|
+
while (cells.length > values.length) {
|
|
573
606
|
disposeCell(cells.pop());
|
|
607
|
+
if (disposed)
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
574
610
|
for (let index = 0; index < values.length; index++) {
|
|
575
611
|
const cell = cells[index];
|
|
576
612
|
if (cell) {
|
|
577
613
|
if (!Object.is(cell.scope.value, values[index]))
|
|
578
614
|
cell.scope.set(values[index]);
|
|
615
|
+
if (disposed)
|
|
616
|
+
return;
|
|
579
617
|
continue;
|
|
580
618
|
}
|
|
581
|
-
const scope = new Scope(values[index], unboundSend, report);
|
|
619
|
+
const scope = new Scope(values[index], unboundSend, report, settlement);
|
|
582
620
|
const fragment = buildFragment(before.parentNode);
|
|
583
621
|
const range = markers(fragment, null);
|
|
584
622
|
try {
|
|
585
623
|
text(scope, fragment, range.end, () => [scope.value], () => scope.value);
|
|
624
|
+
if (disposed) {
|
|
625
|
+
scope.dispose();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
586
628
|
before.parentNode.insertBefore(fragment, before);
|
|
587
629
|
cells.push({ scope, ...range });
|
|
588
630
|
}
|
|
@@ -593,6 +635,9 @@ function mountArray(report, before, initial) {
|
|
|
593
635
|
}
|
|
594
636
|
};
|
|
595
637
|
const dispose = () => {
|
|
638
|
+
if (disposed)
|
|
639
|
+
return;
|
|
640
|
+
disposed = true;
|
|
596
641
|
for (const cell of cells.splice(0))
|
|
597
642
|
disposeCell(cell);
|
|
598
643
|
};
|
|
@@ -617,7 +662,7 @@ export function event(scope, element, name, handler) {
|
|
|
617
662
|
try {
|
|
618
663
|
const result = handler(event);
|
|
619
664
|
if (result !== null && typeof result === 'object' && !scope.disposed) {
|
|
620
|
-
effects ??= eventEffects(scope.report);
|
|
665
|
+
effects ??= eventEffects(scope.report, scope.settlement);
|
|
621
666
|
effects.accept(result);
|
|
622
667
|
}
|
|
623
668
|
}
|
|
@@ -639,29 +684,28 @@ export function event(scope, element, name, handler) {
|
|
|
639
684
|
effects?.dispose();
|
|
640
685
|
});
|
|
641
686
|
}
|
|
642
|
-
/**
|
|
687
|
+
/** The event binding owns its requests; snapshot renders may replace its callback. */
|
|
643
688
|
export function bindEvent(scope, element, name, dependencies, read) {
|
|
644
689
|
let current;
|
|
645
690
|
let owned;
|
|
646
691
|
scope.cleanups.push(() => owned?.dispose());
|
|
647
692
|
scope.watch(dependencies, () => {
|
|
648
693
|
const next = read();
|
|
649
|
-
if (next != null && typeof next !== 'function')
|
|
650
|
-
throw new TypeError(`Event ${name} must be a synchronous handler.`);
|
|
651
694
|
if (Object.is(current, next))
|
|
652
695
|
return;
|
|
653
|
-
owned?.dispose();
|
|
654
|
-
owned = undefined;
|
|
655
696
|
current = next;
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
owned =
|
|
659
|
-
|
|
697
|
+
if (!next) {
|
|
698
|
+
owned?.dispose();
|
|
699
|
+
owned = undefined;
|
|
700
|
+
}
|
|
701
|
+
else if (!owned) {
|
|
702
|
+
owned = new Scope(next, scope.send, scope.report, scope.settlement);
|
|
703
|
+
event(owned, element, name, (event) => current?.(event));
|
|
660
704
|
}
|
|
661
705
|
});
|
|
662
706
|
}
|
|
663
707
|
/** Reconcile merged JSX attributes. Each host, event and control owns its cleanup. */
|
|
664
|
-
export function bindAttributes(scope, element, dependencies, read) {
|
|
708
|
+
export function bindAttributes(scope, element, dependencies, read, sources) {
|
|
665
709
|
const bindings = new Map();
|
|
666
710
|
let previous = {};
|
|
667
711
|
const isEvent = (name) => /^on[A-Z]/u.test(name);
|
|
@@ -676,8 +720,6 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
676
720
|
for (const name of Object.keys(next)) {
|
|
677
721
|
if (['key', 'ref', 'innerHTML', 'children'].includes(name))
|
|
678
722
|
throw new Error(`Spread attribute ${name} is unsupported. Use collections, DOM hosts, or JSX children.`);
|
|
679
|
-
if (isEvent(name) && next[name] != null && typeof next[name] !== 'function')
|
|
680
|
-
throw new Error(`Spread event ${name} must be a synchronous handler.`);
|
|
681
723
|
}
|
|
682
724
|
for (const name of Object.keys(previous)) {
|
|
683
725
|
if (Object.hasOwn(next, name))
|
|
@@ -688,8 +730,11 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
688
730
|
attribute(element, name, undefined);
|
|
689
731
|
}
|
|
690
732
|
for (const [name, value] of Object.entries(next)) {
|
|
733
|
+
if (!Object.hasOwn(previous, name) || !Object.is(previous[name], value))
|
|
734
|
+
traceBinding(sources?.()?.[name], Object.hasOwn(previous, name) ? [previous[name]] : undefined, [value], 'binding');
|
|
691
735
|
if (name !== 'use' && !isEvent(name) && !isControl(name)) {
|
|
692
|
-
|
|
736
|
+
if (!Object.hasOwn(previous, name) || !Object.is(previous[name], value))
|
|
737
|
+
attribute(element, name, value);
|
|
693
738
|
continue;
|
|
694
739
|
}
|
|
695
740
|
let binding = bindings.get(name);
|
|
@@ -699,7 +744,7 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
699
744
|
binding.set(value);
|
|
700
745
|
continue;
|
|
701
746
|
}
|
|
702
|
-
const owned = new Scope(value, scope.send, scope.report);
|
|
747
|
+
const owned = new Scope(value, scope.send, scope.report, scope.settlement);
|
|
703
748
|
bindings.set(name, owned);
|
|
704
749
|
if (name === 'use')
|
|
705
750
|
attach(owned, element, () => [owned.value], () => owned.value);
|
|
@@ -726,7 +771,7 @@ export function branch(scope, parent, before, choose, yes, no) {
|
|
|
726
771
|
if (active !== next) {
|
|
727
772
|
child?.dispose();
|
|
728
773
|
clear(start, end);
|
|
729
|
-
child = new Scope(scope.value, scope.send, scope.report);
|
|
774
|
+
child = new Scope(scope.value, scope.send, scope.report, scope.settlement);
|
|
730
775
|
const fragment = buildFragment(end.parentNode);
|
|
731
776
|
try {
|
|
732
777
|
(next ? yes : no)(child, fragment, null);
|
|
@@ -754,6 +799,8 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
754
799
|
let previousOuter = [];
|
|
755
800
|
let previousIdentities = [];
|
|
756
801
|
const update = () => {
|
|
802
|
+
if (scope.disposed)
|
|
803
|
+
return;
|
|
757
804
|
const target = end.parentNode;
|
|
758
805
|
const next = read();
|
|
759
806
|
const nextOuter = outer();
|
|
@@ -774,8 +821,11 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
774
821
|
target instanceof Element &&
|
|
775
822
|
target.firstChild === rows.get(previousIdentities[0])?.start &&
|
|
776
823
|
target.lastChild === end) {
|
|
777
|
-
for (const row of rows.values())
|
|
824
|
+
for (const row of rows.values()) {
|
|
778
825
|
row.scope.dispose();
|
|
826
|
+
if (scope.disposed)
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
779
829
|
target.replaceChildren(end);
|
|
780
830
|
rows.clear();
|
|
781
831
|
previousList = next;
|
|
@@ -783,28 +833,25 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
783
833
|
previousIdentities = [];
|
|
784
834
|
return;
|
|
785
835
|
}
|
|
786
|
-
const identities = validateIdentities(items, (item, index) =>
|
|
787
|
-
const key = collection ? collection.identity(item, index) : item;
|
|
788
|
-
if (typeof key !== 'string' && typeof key !== 'number')
|
|
789
|
-
throw new Error('Object lists need domain identity. Declare collection(identity) once; do not add JSX keys.');
|
|
790
|
-
return key;
|
|
791
|
-
});
|
|
836
|
+
const identities = validateIdentities(items, (item, index) => collection ? collection.identity(item, index) : item);
|
|
792
837
|
const keep = new Set(identities);
|
|
793
838
|
for (const [key, row] of rows) {
|
|
794
839
|
if (keep.has(key))
|
|
795
840
|
continue;
|
|
796
841
|
row.scope.dispose();
|
|
842
|
+
if (scope.disposed)
|
|
843
|
+
return;
|
|
797
844
|
remove(row.start, row.end);
|
|
798
845
|
rows.delete(key);
|
|
799
846
|
}
|
|
800
847
|
for (let index = 0; index < items.length; index++) {
|
|
801
848
|
const key = identities[index];
|
|
802
849
|
const row = rows.get(key);
|
|
803
|
-
const item =
|
|
850
|
+
const item = items[index];
|
|
804
851
|
if (!row) {
|
|
805
852
|
const fragment = buildFragment(target);
|
|
806
853
|
const range = markers(fragment, null);
|
|
807
|
-
const child = new Scope([item, index], scope.send, scope.report);
|
|
854
|
+
const child = new Scope([item, index], scope.send, scope.report, scope.settlement);
|
|
808
855
|
try {
|
|
809
856
|
build(child, fragment, range.end);
|
|
810
857
|
}
|
|
@@ -812,6 +859,10 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
812
859
|
child.dispose();
|
|
813
860
|
throw error;
|
|
814
861
|
}
|
|
862
|
+
if (scope.disposed) {
|
|
863
|
+
child.dispose();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
815
866
|
// A single element is its own stable range. Keep markers for dynamic/multiple roots.
|
|
816
867
|
const root = range.start.nextSibling;
|
|
817
868
|
const single = root instanceof Element && root.nextSibling === range.end;
|
|
@@ -830,6 +881,8 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
830
881
|
row.scope.value[0] !== item ||
|
|
831
882
|
(indexUsed && row.scope.value[1] !== index)) {
|
|
832
883
|
row.scope.set([item, index]);
|
|
884
|
+
if (scope.disposed)
|
|
885
|
+
return;
|
|
833
886
|
}
|
|
834
887
|
}
|
|
835
888
|
// Retained prefixes are already ordered. New tail rows were appended above and
|
|
@@ -879,18 +932,8 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
879
932
|
});
|
|
880
933
|
update();
|
|
881
934
|
}
|
|
882
|
-
export function invoke(scope, parent, before, dependencies, read, build) {
|
|
883
|
-
const child = new Scope(read(), scope.send, scope.report);
|
|
884
|
-
scope.cleanups.push(() => child.dispose());
|
|
885
|
-
build(child, parent, before);
|
|
886
|
-
scope.watch(dependencies, () => {
|
|
887
|
-
const next = read();
|
|
888
|
-
if (!equal(child.value, next))
|
|
889
|
-
child.set(next);
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
935
|
export function child(scope, parent, before, definition, dependencies, model, send) {
|
|
893
|
-
const child = new Scope(model(), send, scope.report);
|
|
936
|
+
const child = new Scope(model(), send, scope.report, scope.settlement);
|
|
894
937
|
scope.cleanups.push(() => child.dispose());
|
|
895
938
|
definition.build(child, parent, before);
|
|
896
939
|
scope.watch(dependencies, () => {
|
|
@@ -900,11 +943,12 @@ export function child(scope, parent, before, definition, dependencies, model, se
|
|
|
900
943
|
});
|
|
901
944
|
}
|
|
902
945
|
/** A late-bound compiled definition owns one replaceable DOM region. */
|
|
903
|
-
export function viewRegion(scope, parent, before) {
|
|
946
|
+
export function viewRegion(scope, parent, before, options = {}) {
|
|
904
947
|
const { start, end } = markers(parent, before);
|
|
905
948
|
let active;
|
|
906
949
|
let definition;
|
|
907
|
-
|
|
950
|
+
if (!options.manual)
|
|
951
|
+
scope.jobs.push(() => active?.set(scope.value));
|
|
908
952
|
scope.cleanups.push(() => {
|
|
909
953
|
active?.dispose();
|
|
910
954
|
remove(start, end);
|
|
@@ -925,7 +969,7 @@ export function viewRegion(scope, parent, before) {
|
|
|
925
969
|
clear(start, end);
|
|
926
970
|
if (!next)
|
|
927
971
|
return;
|
|
928
|
-
const child = new Scope(scope.value, scope.send, scope.report);
|
|
972
|
+
const child = new Scope(scope.value, scope.send, options.report ?? scope.report, scope.settlement);
|
|
929
973
|
const fragment = buildFragment(end.parentNode);
|
|
930
974
|
active = child;
|
|
931
975
|
definition = next;
|
|
@@ -965,14 +1009,19 @@ export function attach(scope, element, dependencies, read) {
|
|
|
965
1009
|
return;
|
|
966
1010
|
queueMicrotask(() => {
|
|
967
1011
|
if (!scope.disposed && token === generation) {
|
|
1012
|
+
const finished = scope.settlement.begin();
|
|
968
1013
|
try {
|
|
969
|
-
const acquired =
|
|
1014
|
+
const acquired = prepareMount(element, mount, scope.report);
|
|
1015
|
+
Effect.runFork(acquired.closed).addObserver(finished);
|
|
970
1016
|
if (scope.disposed || token !== generation)
|
|
971
1017
|
acquired.dispose();
|
|
972
|
-
else
|
|
1018
|
+
else {
|
|
973
1019
|
active = acquired;
|
|
1020
|
+
acquired.start();
|
|
1021
|
+
}
|
|
974
1022
|
}
|
|
975
1023
|
catch (error) {
|
|
1024
|
+
finished();
|
|
976
1025
|
reportSafely(scope.report, error);
|
|
977
1026
|
}
|
|
978
1027
|
}
|
|
@@ -998,7 +1047,7 @@ export function portal(scope, build, mount = () => undefined) {
|
|
|
998
1047
|
? target.ownerDocument.createElementNS(svgNamespace, 'g')
|
|
999
1048
|
: target.ownerDocument.createElement('div');
|
|
1000
1049
|
host.style.display = 'contents';
|
|
1001
|
-
child = new Scope(scope.value, scope.send, scope.report);
|
|
1050
|
+
child = new Scope(scope.value, scope.send, scope.report, scope.settlement);
|
|
1002
1051
|
target.appendChild(host);
|
|
1003
1052
|
build(child, host, null);
|
|
1004
1053
|
return;
|
package/dist/effectEvent.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
2
|
import { type ReportError } from './errors.js';
|
|
3
|
+
import type { Settlement } from './settlement.js';
|
|
3
4
|
import { type UiRuntime } from './runtime.js';
|
|
4
5
|
declare const tag: unique symbol;
|
|
5
6
|
export interface EffectEventRequest {
|
|
@@ -10,9 +11,9 @@ export interface EffectEventRequest {
|
|
|
10
11
|
/** Capture/prevent native events on every call; the policy gates only Effect execution. */
|
|
11
12
|
export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E>): (event: EventType) => EffectEventRequest;
|
|
12
13
|
export declare function effectEvent<EventType extends Event, E, R>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E, R>, runtime: UiRuntime<R>): (event: EventType) => EffectEventRequest;
|
|
13
|
-
/** Internal listener owner, allocated only when an event returns
|
|
14
|
-
export declare function eventEffects(report: ReportError): {
|
|
15
|
-
accept(
|
|
14
|
+
/** Internal listener owner, allocated only when an event returns an owned request. */
|
|
15
|
+
export declare function eventEffects(report: ReportError, settlement?: Settlement): {
|
|
16
|
+
accept(request: EffectEventRequest): void;
|
|
16
17
|
dispose(): void;
|
|
17
18
|
};
|
|
18
19
|
export {};
|