effectweb 0.3.1 → 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/cache.d.ts +2 -2
- package/dist/cache.js +7 -3
- package/dist/collection.d.ts +1 -1
- package/dist/collection.js +1 -3
- package/dist/component.js +5 -2
- package/dist/dom.d.ts +26 -23
- package/dist/dom.js +150 -119
- package/dist/effectEvent.d.ts +3 -3
- package/dist/effectEvent.js +27 -39
- 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 +1 -1
- package/dist/index.js +1 -1
- package/dist/infinite-query.d.ts +1 -1
- package/dist/infinite-query.js +1 -1
- 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 +5 -4
- package/dist/keyed-tasks.js +27 -22
- package/dist/lazy.d.ts +2 -1
- package/dist/lazy.js +13 -8
- package/dist/load.d.ts +1 -1
- package/dist/load.js +1 -1
- package/dist/mount.d.ts +12 -3
- package/dist/mount.js +63 -20
- package/dist/owner.d.ts +8 -6
- package/dist/owner.js +22 -40
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +2 -1
- package/dist/program.d.ts +7 -5
- package/dist/program.js +33 -20
- package/dist/query-internals.d.ts +1 -1
- package/dist/query.d.ts +1 -1
- package/dist/query.js +0 -2
- package/dist/resource.js +3 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +3 -1
- package/dist/session.js +10 -3
- package/dist/settlement.d.ts +2 -1
- package/dist/settlement.js +13 -8
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +10 -12
- package/dist/task.d.ts +1 -1
- package/dist/task.js +3 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +2 -1
- package/dist/testing.d.ts +5 -4
- package/dist/testing.js +4 -2
- package/package.json +1 -1
- package/dist/intrinsic.d.ts +0 -2
- package/dist/intrinsic.js +0 -120
- package/dist/slotIdentity.d.ts +0 -1
- package/dist/slotIdentity.js +0 -2
package/dist/dom.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { compiledSlots } from './slotIdentity.js';
|
|
2
1
|
import attributeData from './dom-attributes.json' with { type: 'json' };
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
3
|
import { validateIdentities } from './collection.js';
|
|
4
4
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
5
5
|
import { eventEffects } from './effectEvent.js';
|
|
6
6
|
import { traceBinding } from './diagnostics.js';
|
|
7
|
-
import { shareData } from './sharing.js';
|
|
8
7
|
import { jsxComponent } from './jsx.js';
|
|
9
8
|
import { Settlement } from './settlement.js';
|
|
10
|
-
import {
|
|
9
|
+
import { prepareMount } from './mount.js';
|
|
10
|
+
const bindingLocation = (source) => typeof source === 'function' ? source() : source;
|
|
11
11
|
const equal = (a, b) => a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
|
|
12
12
|
// A model publication reconciles DOM first and controlled dependent properties second.
|
|
13
13
|
// Nested child scopes join the same synchronous commit; there is no reactive graph.
|
|
@@ -42,41 +42,23 @@ export class Scope {
|
|
|
42
42
|
jobs = [];
|
|
43
43
|
cleanups = [];
|
|
44
44
|
disposed = false;
|
|
45
|
-
revision = 0;
|
|
46
45
|
constructor(value, send, report = reportError, settlement = new Settlement()) {
|
|
47
46
|
this.value = value;
|
|
48
47
|
this.send = send;
|
|
49
48
|
this.report = report;
|
|
50
49
|
this.settlement = settlement;
|
|
51
50
|
}
|
|
52
|
-
derive(dependencies, compute, source) {
|
|
53
|
-
let previous;
|
|
54
|
-
let value;
|
|
55
|
-
let revision = -1;
|
|
56
|
-
return () => {
|
|
57
|
-
if (revision === this.revision)
|
|
58
|
-
return value;
|
|
59
|
-
const next = dependencies();
|
|
60
|
-
if (!previous || !equal(previous, next)) {
|
|
61
|
-
value = previous ? shareData(value, compute()) : compute();
|
|
62
|
-
traceBinding(source, previous, next, 'derive');
|
|
63
|
-
previous = next;
|
|
64
|
-
}
|
|
65
|
-
revision = this.revision;
|
|
66
|
-
return value;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
51
|
watch(dependencies, apply, source) {
|
|
70
52
|
let previous = dependencies();
|
|
71
53
|
apply();
|
|
72
|
-
traceBinding(source, undefined, previous, 'binding');
|
|
54
|
+
traceBinding(bindingLocation(source), undefined, previous, 'binding');
|
|
73
55
|
if (!previous.length)
|
|
74
56
|
return;
|
|
75
57
|
const run = () => {
|
|
76
58
|
const next = dependencies();
|
|
77
59
|
if (!equal(previous, next)) {
|
|
78
60
|
apply();
|
|
79
|
-
traceBinding(source, previous, next, 'binding');
|
|
61
|
+
traceBinding(bindingLocation(source), previous, next, 'binding');
|
|
80
62
|
previous = next;
|
|
81
63
|
}
|
|
82
64
|
};
|
|
@@ -87,7 +69,6 @@ export class Scope {
|
|
|
87
69
|
return;
|
|
88
70
|
commitDom(() => {
|
|
89
71
|
this.value = value;
|
|
90
|
-
this.revision++;
|
|
91
72
|
for (const job of this.jobs) {
|
|
92
73
|
if (this.disposed)
|
|
93
74
|
break;
|
|
@@ -108,18 +89,18 @@ export class Scope {
|
|
|
108
89
|
runAll(this.cleanups.splice(0).reverse(), this.report);
|
|
109
90
|
}
|
|
110
91
|
}
|
|
111
|
-
/**
|
|
112
|
-
export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(
|
|
113
|
-
|
|
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 });
|
|
114
95
|
}, { [jsxComponent]: true });
|
|
115
96
|
/** Untyped callers get an actionable failure instead of silently losing child messages. */
|
|
116
97
|
export function unboundSend(_message) {
|
|
117
98
|
throw new Error('This view needs a dispatcher. Mount it through ViewBinding with model and send.');
|
|
118
99
|
}
|
|
119
100
|
const contentBrand = Symbol('compiled content');
|
|
120
|
-
|
|
121
|
-
export function slot(
|
|
122
|
-
|
|
101
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
102
|
+
export function slot(render) {
|
|
103
|
+
return render;
|
|
123
104
|
}
|
|
124
105
|
// Placement values are opaque to structural sharing, which only traverses plain data.
|
|
125
106
|
class SlotPlacement {
|
|
@@ -136,62 +117,83 @@ function isContent(value) {
|
|
|
136
117
|
(typeof value === 'object' || typeof value === 'function') &&
|
|
137
118
|
contentBrand in value);
|
|
138
119
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
});
|
|
146
|
-
owner.cleanups.push(() => {
|
|
147
|
-
for (const scope of placements)
|
|
148
|
-
scope.dispose();
|
|
149
|
-
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);
|
|
150
126
|
});
|
|
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 {
|
|
152
149
|
mount(parent, before, value, report, settlement) {
|
|
153
|
-
|
|
154
|
-
throw new Error('Cannot mount content after its declaring view disposed');
|
|
155
|
-
const scope = new Scope(value, owner.send, report, settlement);
|
|
150
|
+
const scope = new Scope(value, unboundSend, report, settlement);
|
|
156
151
|
const fragment = buildFragment(parent);
|
|
157
152
|
const range = markers(fragment, null);
|
|
158
153
|
scope.cleanups.push(() => remove(range.start, range.end));
|
|
159
154
|
try {
|
|
160
155
|
build(scope, fragment, range.end);
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
if (!scope.disposed)
|
|
157
|
+
parent.insertBefore(fragment, before);
|
|
163
158
|
}
|
|
164
159
|
catch (error) {
|
|
165
160
|
scope.dispose();
|
|
166
161
|
throw error;
|
|
167
162
|
}
|
|
168
|
-
return {
|
|
169
|
-
set(value) {
|
|
170
|
-
if (!Object.is(scope.value, value))
|
|
171
|
-
scope.set(value);
|
|
172
|
-
},
|
|
173
|
-
dispose() {
|
|
174
|
-
placements.delete(scope);
|
|
175
|
-
scope.dispose();
|
|
176
|
-
},
|
|
177
|
-
};
|
|
163
|
+
return { set: (value) => scope.set(value), dispose: () => scope.dispose() };
|
|
178
164
|
},
|
|
179
165
|
};
|
|
180
|
-
const slot = Object.assign((value) => new SlotPlacement(definition, value), {
|
|
181
|
-
[contentBrand]: true,
|
|
182
|
-
definition,
|
|
183
|
-
});
|
|
184
|
-
compiledSlots.add(slot);
|
|
185
|
-
return slot;
|
|
186
166
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
190
186
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 });
|
|
195
197
|
}
|
|
196
198
|
/** Owned content in an element target, defaulting to the document body. */
|
|
197
199
|
export const Portal = /* @__PURE__ */ compiled((scope) => {
|
|
@@ -263,15 +265,23 @@ export function mountView(parent, definition, source, options = {}) {
|
|
|
263
265
|
const { start, end } = markers(parent, null);
|
|
264
266
|
const scope = new Scope(source.model(), source.send, options.onError);
|
|
265
267
|
let unsubscribe = () => { };
|
|
268
|
+
let ready = false;
|
|
269
|
+
let latest = scope.value;
|
|
266
270
|
try {
|
|
271
|
+
unsubscribe = source.subscribe((model) => {
|
|
272
|
+
latest = model;
|
|
273
|
+
if (ready)
|
|
274
|
+
scope.set(model);
|
|
275
|
+
});
|
|
267
276
|
const fragment = buildFragment(parent);
|
|
268
277
|
definition.build(scope, fragment, null);
|
|
269
278
|
parent.insertBefore(fragment, end);
|
|
270
|
-
|
|
279
|
+
ready = true;
|
|
280
|
+
if (!Object.is(latest, scope.value))
|
|
281
|
+
scope.set(latest);
|
|
271
282
|
}
|
|
272
283
|
catch (error) {
|
|
273
|
-
scope.dispose();
|
|
274
|
-
remove(start, end);
|
|
284
|
+
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
275
285
|
throw error;
|
|
276
286
|
}
|
|
277
287
|
const dispose = () => {
|
|
@@ -282,8 +292,10 @@ export function mountView(parent, definition, source, options = {}) {
|
|
|
282
292
|
return Object.assign(dispose, {
|
|
283
293
|
dispose,
|
|
284
294
|
close: () => {
|
|
285
|
-
|
|
286
|
-
|
|
295
|
+
return Effect.suspend(() => {
|
|
296
|
+
dispose();
|
|
297
|
+
return scope.settlement.wait();
|
|
298
|
+
});
|
|
287
299
|
},
|
|
288
300
|
});
|
|
289
301
|
});
|
|
@@ -385,7 +397,7 @@ export function bindAttribute(scope, element, name, dependencies, read, source)
|
|
|
385
397
|
let previous = dependencies();
|
|
386
398
|
let value = read();
|
|
387
399
|
attribute(element, name, value);
|
|
388
|
-
traceBinding(source, undefined, previous, 'binding');
|
|
400
|
+
traceBinding(bindingLocation(source), undefined, previous, 'binding');
|
|
389
401
|
if (!previous.length)
|
|
390
402
|
return;
|
|
391
403
|
// Keep the value cache in the dependency job itself, without an extra closure
|
|
@@ -399,7 +411,7 @@ export function bindAttribute(scope, element, name, dependencies, read, source)
|
|
|
399
411
|
attribute(element, name, nextValue);
|
|
400
412
|
value = nextValue;
|
|
401
413
|
}
|
|
402
|
-
traceBinding(source, previous, next, 'binding');
|
|
414
|
+
traceBinding(bindingLocation(source), previous, next, 'binding');
|
|
403
415
|
previous = next;
|
|
404
416
|
});
|
|
405
417
|
}
|
|
@@ -519,16 +531,20 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
519
531
|
let value = read();
|
|
520
532
|
if (Array.isArray(value)) {
|
|
521
533
|
validateContent(value);
|
|
522
|
-
value =
|
|
534
|
+
value = new SlotPlacement(arrayContent, value);
|
|
523
535
|
}
|
|
524
536
|
if (isContent(value)) {
|
|
525
|
-
node.data
|
|
537
|
+
if (node.data !== '')
|
|
538
|
+
node.data = '';
|
|
526
539
|
if (content?.definition === value.definition) {
|
|
527
540
|
content.mounted.set(value.value);
|
|
528
541
|
return;
|
|
529
542
|
}
|
|
530
|
-
content
|
|
543
|
+
const previous = content;
|
|
531
544
|
content = undefined;
|
|
545
|
+
previous?.mounted.dispose();
|
|
546
|
+
if (scope.disposed)
|
|
547
|
+
return;
|
|
532
548
|
if (start)
|
|
533
549
|
clear(start, node);
|
|
534
550
|
else {
|
|
@@ -536,10 +552,11 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
536
552
|
scope.cleanups.push(() => content?.mounted.dispose());
|
|
537
553
|
node.parentNode.insertBefore(start, node);
|
|
538
554
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
mounted
|
|
542
|
-
|
|
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 };
|
|
543
560
|
return;
|
|
544
561
|
}
|
|
545
562
|
if (value != null && typeof value === 'object')
|
|
@@ -575,20 +592,28 @@ const arrayContent = {
|
|
|
575
592
|
/** Runtime content arrays are positional; domain lists retain the explicit collection contract. */
|
|
576
593
|
function mountArray(report, before, initial, settlement) {
|
|
577
594
|
const cells = [];
|
|
595
|
+
let disposed = false;
|
|
578
596
|
const disposeCell = (cell) => {
|
|
579
597
|
cell.scope.dispose();
|
|
580
598
|
remove(cell.start, cell.end);
|
|
581
599
|
};
|
|
582
600
|
const set = (input) => {
|
|
601
|
+
if (disposed)
|
|
602
|
+
return;
|
|
583
603
|
const values = input;
|
|
584
604
|
validateContent(values);
|
|
585
|
-
while (cells.length > values.length)
|
|
605
|
+
while (cells.length > values.length) {
|
|
586
606
|
disposeCell(cells.pop());
|
|
607
|
+
if (disposed)
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
587
610
|
for (let index = 0; index < values.length; index++) {
|
|
588
611
|
const cell = cells[index];
|
|
589
612
|
if (cell) {
|
|
590
613
|
if (!Object.is(cell.scope.value, values[index]))
|
|
591
614
|
cell.scope.set(values[index]);
|
|
615
|
+
if (disposed)
|
|
616
|
+
return;
|
|
592
617
|
continue;
|
|
593
618
|
}
|
|
594
619
|
const scope = new Scope(values[index], unboundSend, report, settlement);
|
|
@@ -596,6 +621,10 @@ function mountArray(report, before, initial, settlement) {
|
|
|
596
621
|
const range = markers(fragment, null);
|
|
597
622
|
try {
|
|
598
623
|
text(scope, fragment, range.end, () => [scope.value], () => scope.value);
|
|
624
|
+
if (disposed) {
|
|
625
|
+
scope.dispose();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
599
628
|
before.parentNode.insertBefore(fragment, before);
|
|
600
629
|
cells.push({ scope, ...range });
|
|
601
630
|
}
|
|
@@ -606,6 +635,9 @@ function mountArray(report, before, initial, settlement) {
|
|
|
606
635
|
}
|
|
607
636
|
};
|
|
608
637
|
const dispose = () => {
|
|
638
|
+
if (disposed)
|
|
639
|
+
return;
|
|
640
|
+
disposed = true;
|
|
609
641
|
for (const cell of cells.splice(0))
|
|
610
642
|
disposeCell(cell);
|
|
611
643
|
};
|
|
@@ -652,29 +684,28 @@ export function event(scope, element, name, handler) {
|
|
|
652
684
|
effects?.dispose();
|
|
653
685
|
});
|
|
654
686
|
}
|
|
655
|
-
/**
|
|
687
|
+
/** The event binding owns its requests; snapshot renders may replace its callback. */
|
|
656
688
|
export function bindEvent(scope, element, name, dependencies, read) {
|
|
657
689
|
let current;
|
|
658
690
|
let owned;
|
|
659
691
|
scope.cleanups.push(() => owned?.dispose());
|
|
660
692
|
scope.watch(dependencies, () => {
|
|
661
693
|
const next = read();
|
|
662
|
-
if (next != null && typeof next !== 'function')
|
|
663
|
-
throw new TypeError(`Event ${name} must be a synchronous handler.`);
|
|
664
694
|
if (Object.is(current, next))
|
|
665
695
|
return;
|
|
666
|
-
owned?.dispose();
|
|
667
|
-
owned = undefined;
|
|
668
696
|
current = next;
|
|
669
|
-
if (
|
|
670
|
-
|
|
697
|
+
if (!next) {
|
|
698
|
+
owned?.dispose();
|
|
699
|
+
owned = undefined;
|
|
700
|
+
}
|
|
701
|
+
else if (!owned) {
|
|
671
702
|
owned = new Scope(next, scope.send, scope.report, scope.settlement);
|
|
672
|
-
event(owned, element, name,
|
|
703
|
+
event(owned, element, name, (event) => current?.(event));
|
|
673
704
|
}
|
|
674
705
|
});
|
|
675
706
|
}
|
|
676
707
|
/** Reconcile merged JSX attributes. Each host, event and control owns its cleanup. */
|
|
677
|
-
export function bindAttributes(scope, element, dependencies, read) {
|
|
708
|
+
export function bindAttributes(scope, element, dependencies, read, sources) {
|
|
678
709
|
const bindings = new Map();
|
|
679
710
|
let previous = {};
|
|
680
711
|
const isEvent = (name) => /^on[A-Z]/u.test(name);
|
|
@@ -689,8 +720,6 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
689
720
|
for (const name of Object.keys(next)) {
|
|
690
721
|
if (['key', 'ref', 'innerHTML', 'children'].includes(name))
|
|
691
722
|
throw new Error(`Spread attribute ${name} is unsupported. Use collections, DOM hosts, or JSX children.`);
|
|
692
|
-
if (isEvent(name) && next[name] != null && typeof next[name] !== 'function')
|
|
693
|
-
throw new Error(`Spread event ${name} must be a synchronous handler.`);
|
|
694
723
|
}
|
|
695
724
|
for (const name of Object.keys(previous)) {
|
|
696
725
|
if (Object.hasOwn(next, name))
|
|
@@ -701,8 +730,11 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
701
730
|
attribute(element, name, undefined);
|
|
702
731
|
}
|
|
703
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');
|
|
704
735
|
if (name !== 'use' && !isEvent(name) && !isControl(name)) {
|
|
705
|
-
|
|
736
|
+
if (!Object.hasOwn(previous, name) || !Object.is(previous[name], value))
|
|
737
|
+
attribute(element, name, value);
|
|
706
738
|
continue;
|
|
707
739
|
}
|
|
708
740
|
let binding = bindings.get(name);
|
|
@@ -767,6 +799,8 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
767
799
|
let previousOuter = [];
|
|
768
800
|
let previousIdentities = [];
|
|
769
801
|
const update = () => {
|
|
802
|
+
if (scope.disposed)
|
|
803
|
+
return;
|
|
770
804
|
const target = end.parentNode;
|
|
771
805
|
const next = read();
|
|
772
806
|
const nextOuter = outer();
|
|
@@ -787,8 +821,11 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
787
821
|
target instanceof Element &&
|
|
788
822
|
target.firstChild === rows.get(previousIdentities[0])?.start &&
|
|
789
823
|
target.lastChild === end) {
|
|
790
|
-
for (const row of rows.values())
|
|
824
|
+
for (const row of rows.values()) {
|
|
791
825
|
row.scope.dispose();
|
|
826
|
+
if (scope.disposed)
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
792
829
|
target.replaceChildren(end);
|
|
793
830
|
rows.clear();
|
|
794
831
|
previousList = next;
|
|
@@ -796,24 +833,21 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
796
833
|
previousIdentities = [];
|
|
797
834
|
return;
|
|
798
835
|
}
|
|
799
|
-
const identities = validateIdentities(items, (item, index) =>
|
|
800
|
-
const key = collection ? collection.identity(item, index) : item;
|
|
801
|
-
if (typeof key !== 'string' && typeof key !== 'number')
|
|
802
|
-
throw new Error('Object lists need domain identity. Declare collection(identity) once; do not add JSX keys.');
|
|
803
|
-
return key;
|
|
804
|
-
});
|
|
836
|
+
const identities = validateIdentities(items, (item, index) => collection ? collection.identity(item, index) : item);
|
|
805
837
|
const keep = new Set(identities);
|
|
806
838
|
for (const [key, row] of rows) {
|
|
807
839
|
if (keep.has(key))
|
|
808
840
|
continue;
|
|
809
841
|
row.scope.dispose();
|
|
842
|
+
if (scope.disposed)
|
|
843
|
+
return;
|
|
810
844
|
remove(row.start, row.end);
|
|
811
845
|
rows.delete(key);
|
|
812
846
|
}
|
|
813
847
|
for (let index = 0; index < items.length; index++) {
|
|
814
848
|
const key = identities[index];
|
|
815
849
|
const row = rows.get(key);
|
|
816
|
-
const item =
|
|
850
|
+
const item = items[index];
|
|
817
851
|
if (!row) {
|
|
818
852
|
const fragment = buildFragment(target);
|
|
819
853
|
const range = markers(fragment, null);
|
|
@@ -825,6 +859,10 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
825
859
|
child.dispose();
|
|
826
860
|
throw error;
|
|
827
861
|
}
|
|
862
|
+
if (scope.disposed) {
|
|
863
|
+
child.dispose();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
828
866
|
// A single element is its own stable range. Keep markers for dynamic/multiple roots.
|
|
829
867
|
const root = range.start.nextSibling;
|
|
830
868
|
const single = root instanceof Element && root.nextSibling === range.end;
|
|
@@ -843,6 +881,8 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
843
881
|
row.scope.value[0] !== item ||
|
|
844
882
|
(indexUsed && row.scope.value[1] !== index)) {
|
|
845
883
|
row.scope.set([item, index]);
|
|
884
|
+
if (scope.disposed)
|
|
885
|
+
return;
|
|
846
886
|
}
|
|
847
887
|
}
|
|
848
888
|
// Retained prefixes are already ordered. New tail rows were appended above and
|
|
@@ -892,16 +932,6 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
892
932
|
});
|
|
893
933
|
update();
|
|
894
934
|
}
|
|
895
|
-
export function invoke(scope, parent, before, dependencies, read, build) {
|
|
896
|
-
const child = new Scope(read(), scope.send, scope.report, scope.settlement);
|
|
897
|
-
scope.cleanups.push(() => child.dispose());
|
|
898
|
-
build(child, parent, before);
|
|
899
|
-
scope.watch(dependencies, () => {
|
|
900
|
-
const next = read();
|
|
901
|
-
if (!equal(child.value, next))
|
|
902
|
-
child.set(next);
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
935
|
export function child(scope, parent, before, definition, dependencies, model, send) {
|
|
906
936
|
const child = new Scope(model(), send, scope.report, scope.settlement);
|
|
907
937
|
scope.cleanups.push(() => child.dispose());
|
|
@@ -981,12 +1011,14 @@ export function attach(scope, element, dependencies, read) {
|
|
|
981
1011
|
if (!scope.disposed && token === generation) {
|
|
982
1012
|
const finished = scope.settlement.begin();
|
|
983
1013
|
try {
|
|
984
|
-
const acquired =
|
|
985
|
-
acquired.closed.
|
|
1014
|
+
const acquired = prepareMount(element, mount, scope.report);
|
|
1015
|
+
Effect.runFork(acquired.closed).addObserver(finished);
|
|
986
1016
|
if (scope.disposed || token !== generation)
|
|
987
1017
|
acquired.dispose();
|
|
988
|
-
else
|
|
1018
|
+
else {
|
|
989
1019
|
active = acquired;
|
|
1020
|
+
acquired.start();
|
|
1021
|
+
}
|
|
990
1022
|
}
|
|
991
1023
|
catch (error) {
|
|
992
1024
|
finished();
|
|
@@ -1106,4 +1138,3 @@ export function template(build, depth = 0) {
|
|
|
1106
1138
|
export function literal(parent, before, value) {
|
|
1107
1139
|
parent.insertBefore((parent.ownerDocument ?? document).createTextNode(value), before);
|
|
1108
1140
|
}
|
|
1109
|
-
export { intrinsic } from './intrinsic.js';
|
package/dist/effectEvent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
2
|
import { type ReportError } from './errors.js';
|
|
3
3
|
import type { Settlement } from './settlement.js';
|
|
4
4
|
import { type UiRuntime } from './runtime.js';
|
|
@@ -11,9 +11,9 @@ export interface EffectEventRequest {
|
|
|
11
11
|
/** Capture/prevent native events on every call; the policy gates only Effect execution. */
|
|
12
12
|
export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E>): (event: EventType) => EffectEventRequest;
|
|
13
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;
|
|
14
|
-
/** Internal listener owner, allocated only when an event returns
|
|
14
|
+
/** Internal listener owner, allocated only when an event returns an owned request. */
|
|
15
15
|
export declare function eventEffects(report: ReportError, settlement?: Settlement): {
|
|
16
|
-
accept(
|
|
16
|
+
accept(request: EffectEventRequest): void;
|
|
17
17
|
dispose(): void;
|
|
18
18
|
};
|
|
19
19
|
export {};
|
package/dist/effectEvent.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Fiber from 'effect/Fiber';
|
|
2
4
|
import { reportSafely } from './errors.js';
|
|
3
5
|
import { defaultUiRuntime } from './runtime.js';
|
|
4
6
|
const tag = Symbol('Effect event');
|
|
@@ -6,7 +8,7 @@ export function effectEvent(policy, load, runtime) {
|
|
|
6
8
|
const owner = runtime ?? defaultUiRuntime;
|
|
7
9
|
return (event) => ({ [tag]: true, policy, effect: owner.provide(load(event)) });
|
|
8
10
|
}
|
|
9
|
-
/** Internal listener owner, allocated only when an event returns
|
|
11
|
+
/** Internal listener owner, allocated only when an event returns an owned request. */
|
|
10
12
|
export function eventEffects(report, settlement) {
|
|
11
13
|
let active;
|
|
12
14
|
let disposed = false;
|
|
@@ -17,49 +19,35 @@ export function eventEffects(report, settlement) {
|
|
|
17
19
|
Effect.runFork(Fiber.interrupt(previous.fiber));
|
|
18
20
|
};
|
|
19
21
|
return {
|
|
20
|
-
accept(
|
|
22
|
+
accept(request) {
|
|
21
23
|
if (disposed)
|
|
22
24
|
return;
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (disposed || active !== token) {
|
|
36
|
-
Effect.runFork(Fiber.interrupt(fiber));
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
fiber.addObserver((exit) => {
|
|
40
|
-
if (disposed || active !== token)
|
|
41
|
-
return;
|
|
25
|
+
if (request.policy === 'drop' && active)
|
|
26
|
+
return;
|
|
27
|
+
stop();
|
|
28
|
+
const token = {};
|
|
29
|
+
active = token;
|
|
30
|
+
const finished = settlement?.begin();
|
|
31
|
+
try {
|
|
32
|
+
const fiber = Effect.runFork(request.effect);
|
|
33
|
+
token.fiber = fiber;
|
|
34
|
+
fiber.addObserver((exit) => {
|
|
35
|
+
const current = !disposed && active === token;
|
|
36
|
+
if (current)
|
|
42
37
|
active = undefined;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
38
|
+
if (exit._tag === 'Failure' && (current || !Cause.hasInterruptsOnly(exit.cause)))
|
|
39
|
+
reportSafely(report, exit.cause);
|
|
48
40
|
finished?.();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
});
|
|
42
|
+
if (disposed || active !== token) {
|
|
43
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
52
44
|
}
|
|
53
45
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'then' in value &&
|
|
60
|
-
typeof value.then === 'function') {
|
|
61
|
-
void Promise.resolve(value).catch((error) => reportSafely(report, error));
|
|
62
|
-
reportSafely(report, new Error('Event returned an unowned Promise. Adapt it with fromPromise inside effectEvent or a command.'));
|
|
46
|
+
catch (error) {
|
|
47
|
+
finished?.();
|
|
48
|
+
if (active === token)
|
|
49
|
+
active = undefined;
|
|
50
|
+
reportSafely(report, error);
|
|
63
51
|
}
|
|
64
52
|
},
|
|
65
53
|
dispose() {
|
package/dist/form.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type * as Cause from 'effect/Cause';
|
|
2
|
+
import type * as Effect from 'effect/Effect';
|
|
2
3
|
import { type Transition } from './program.js';
|
|
3
4
|
import type { Snapshot } from './snapshot.js';
|
|
4
5
|
import type { EffectEventRequest } from './effectEvent.js';
|
package/dist/http.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import type * as Schema from 'effect/Schema';
|
|
2
3
|
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
3
4
|
import type * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
|
|
4
5
|
/** Decode successful JSON through a schema, retaining transport, decoding and service types. */
|
package/dist/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
2
|
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
3
3
|
import * as HttpClientResponse from 'effect/unstable/http/HttpClientResponse';
|
|
4
4
|
/** Decode successful JSON through a schema, retaining transport, decoding and service types. */
|