effectweb 0.3.1 → 0.5.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/dist/AsyncContent.d.ts +1 -1
- package/dist/AsyncContent.js +18 -35
- package/dist/cache-internals.d.ts +3 -2
- package/dist/cache.d.ts +15 -12
- package/dist/cache.js +51 -10
- package/dist/collection.d.ts +1 -1
- package/dist/collection.js +1 -3
- package/dist/component.d.ts +1 -1
- package/dist/component.js +9 -4
- package/dist/dom.d.ts +64 -31
- package/dist/dom.js +237 -121
- package/dist/effectEvent.d.ts +10 -7
- package/dist/effectEvent.js +50 -46
- 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 +10 -8
- package/dist/index.js +10 -8
- package/dist/infinite-query.d.ts +12 -8
- package/dist/infinite-query.js +21 -13
- package/dist/jsx-runtime.d.ts +6 -0
- package/dist/jsx-runtime.js +14 -1
- package/dist/jsx.d.ts +4 -1
- package/dist/keyed-tasks.d.ts +8 -6
- package/dist/keyed-tasks.js +29 -24
- package/dist/lazy.d.ts +4 -2
- package/dist/lazy.js +16 -10
- package/dist/load.d.ts +1 -1
- package/dist/load.js +1 -1
- package/dist/mount.d.ts +23 -8
- package/dist/mount.js +116 -27
- package/dist/owner.d.ts +13 -8
- package/dist/owner.js +37 -41
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +2 -1
- package/dist/program.d.ts +20 -8
- package/dist/program.js +44 -22
- package/dist/query-internals.d.ts +5 -3
- package/dist/query.d.ts +17 -6
- package/dist/query.js +6 -3
- package/dist/render.d.ts +17 -0
- package/dist/render.js +48 -0
- package/dist/resource.d.ts +4 -3
- package/dist/resource.js +12 -6
- package/dist/runtime.d.ts +12 -1
- package/dist/runtime.js +27 -5
- package/dist/session.d.ts +3 -2
- package/dist/session.js +15 -5
- package/dist/settlement.d.ts +8 -1
- package/dist/settlement.js +20 -8
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +10 -12
- package/dist/source.d.ts +38 -0
- package/dist/source.js +218 -0
- package/dist/task.d.ts +1 -1
- package/dist/task.js +3 -1
- package/dist/tasks.d.ts +9 -6
- package/dist/tasks.js +7 -4
- package/dist/testing.d.ts +5 -4
- package/dist/testing.js +4 -2
- package/package.json +10 -3
- package/README.md +0 -101
- 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,15 @@
|
|
|
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
|
+
import * as Exit from 'effect/Exit';
|
|
4
|
+
import { protectSnapshot } from './snapshot.js';
|
|
3
5
|
import { validateIdentities } from './collection.js';
|
|
4
6
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
5
7
|
import { eventEffects } from './effectEvent.js';
|
|
6
8
|
import { traceBinding } from './diagnostics.js';
|
|
7
|
-
import { shareData } from './sharing.js';
|
|
8
9
|
import { jsxComponent } from './jsx.js';
|
|
9
10
|
import { Settlement } from './settlement.js';
|
|
10
|
-
import {
|
|
11
|
+
import { prepareMount } from './mount.js';
|
|
12
|
+
const bindingLocation = (source) => typeof source === 'function' ? source() : source;
|
|
11
13
|
const equal = (a, b) => a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
|
|
12
14
|
// A model publication reconciles DOM first and controlled dependent properties second.
|
|
13
15
|
// Nested child scopes join the same synchronous commit; there is no reactive graph.
|
|
@@ -42,41 +44,23 @@ export class Scope {
|
|
|
42
44
|
jobs = [];
|
|
43
45
|
cleanups = [];
|
|
44
46
|
disposed = false;
|
|
45
|
-
revision = 0;
|
|
46
47
|
constructor(value, send, report = reportError, settlement = new Settlement()) {
|
|
47
48
|
this.value = value;
|
|
48
49
|
this.send = send;
|
|
49
50
|
this.report = report;
|
|
50
51
|
this.settlement = settlement;
|
|
51
52
|
}
|
|
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
53
|
watch(dependencies, apply, source) {
|
|
70
54
|
let previous = dependencies();
|
|
71
55
|
apply();
|
|
72
|
-
traceBinding(source, undefined, previous, 'binding');
|
|
56
|
+
traceBinding(bindingLocation(source), undefined, previous, 'binding');
|
|
73
57
|
if (!previous.length)
|
|
74
58
|
return;
|
|
75
59
|
const run = () => {
|
|
76
60
|
const next = dependencies();
|
|
77
61
|
if (!equal(previous, next)) {
|
|
78
62
|
apply();
|
|
79
|
-
traceBinding(source, previous, next, 'binding');
|
|
63
|
+
traceBinding(bindingLocation(source), previous, next, 'binding');
|
|
80
64
|
previous = next;
|
|
81
65
|
}
|
|
82
66
|
};
|
|
@@ -87,7 +71,6 @@ export class Scope {
|
|
|
87
71
|
return;
|
|
88
72
|
commitDom(() => {
|
|
89
73
|
this.value = value;
|
|
90
|
-
this.revision++;
|
|
91
74
|
for (const job of this.jobs) {
|
|
92
75
|
if (this.disposed)
|
|
93
76
|
break;
|
|
@@ -108,18 +91,18 @@ export class Scope {
|
|
|
108
91
|
runAll(this.cleanups.splice(0).reverse(), this.report);
|
|
109
92
|
}
|
|
110
93
|
}
|
|
111
|
-
/**
|
|
112
|
-
export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(
|
|
113
|
-
|
|
94
|
+
/** Mount a view with an explicit model and message dispatcher. */
|
|
95
|
+
export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(props) {
|
|
96
|
+
return new SlotPlacement(viewContent(props.view), { model: props.model, send: props.send });
|
|
114
97
|
}, { [jsxComponent]: true });
|
|
115
98
|
/** Untyped callers get an actionable failure instead of silently losing child messages. */
|
|
116
99
|
export function unboundSend(_message) {
|
|
117
100
|
throw new Error('This view needs a dispatcher. Mount it through ViewBinding with model and send.');
|
|
118
101
|
}
|
|
119
102
|
const contentBrand = Symbol('compiled content');
|
|
120
|
-
|
|
121
|
-
export function slot(
|
|
122
|
-
|
|
103
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
104
|
+
export function slot(render) {
|
|
105
|
+
return render;
|
|
123
106
|
}
|
|
124
107
|
// Placement values are opaque to structural sharing, which only traverses plain data.
|
|
125
108
|
class SlotPlacement {
|
|
@@ -136,62 +119,152 @@ function isContent(value) {
|
|
|
136
119
|
(typeof value === 'object' || typeof value === 'function') &&
|
|
137
120
|
contentBrand in value);
|
|
138
121
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
122
|
+
/** Evaluate ordinary synchronous JavaScript for each immutable model publication. */
|
|
123
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
124
|
+
export function view(render) {
|
|
125
|
+
return compiled((scope, parent, before) => {
|
|
126
|
+
const read = () => render(scope.value, scope.send);
|
|
127
|
+
text(scope, parent, before, () => [scope.value], read);
|
|
145
128
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
129
|
+
}
|
|
130
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
131
|
+
export function compiled(build) {
|
|
132
|
+
const definition = Object.assign((props) => new SlotPlacement(viewContent(definition), { model: props, send: unboundSend }), { build, [jsxComponent]: true });
|
|
133
|
+
return definition;
|
|
134
|
+
}
|
|
135
|
+
/** Skip a view only under the caller's explicit equality contract. */
|
|
136
|
+
export function memoView(definition, equals) {
|
|
137
|
+
return compiled((scope, parent, before) => {
|
|
138
|
+
const child = new Scope(scope.value, scope.send, scope.report, scope.settlement);
|
|
139
|
+
scope.cleanups.push(() => child.dispose());
|
|
140
|
+
definition.build(child, parent, before);
|
|
141
|
+
scope.jobs.push(() => {
|
|
142
|
+
if (!equals(child.value, scope.value))
|
|
143
|
+
child.set(scope.value);
|
|
144
|
+
});
|
|
150
145
|
});
|
|
151
|
-
|
|
146
|
+
}
|
|
147
|
+
const viewDefinitions = new WeakMap();
|
|
148
|
+
function viewContent(definition) {
|
|
149
|
+
let content = viewDefinitions.get(definition);
|
|
150
|
+
if (!content) {
|
|
151
|
+
content = contentDefinition((scope, parent, before) => {
|
|
152
|
+
const childScope = new Scope(scope.value.model, (message) => scope.value.send(message), scope.report, scope.settlement);
|
|
153
|
+
scope.cleanups.push(() => childScope.dispose());
|
|
154
|
+
definition.build(childScope, parent, before);
|
|
155
|
+
scope.jobs.push(() => childScope.set(scope.value.model));
|
|
156
|
+
});
|
|
157
|
+
viewDefinitions.set(definition, content);
|
|
158
|
+
}
|
|
159
|
+
return content;
|
|
160
|
+
}
|
|
161
|
+
function contentDefinition(build) {
|
|
162
|
+
return {
|
|
152
163
|
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);
|
|
164
|
+
const scope = new Scope(value, unboundSend, report, settlement);
|
|
156
165
|
const fragment = buildFragment(parent);
|
|
157
166
|
const range = markers(fragment, null);
|
|
158
167
|
scope.cleanups.push(() => remove(range.start, range.end));
|
|
159
168
|
try {
|
|
160
169
|
build(scope, fragment, range.end);
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
if (!scope.disposed)
|
|
171
|
+
parent.insertBefore(fragment, before);
|
|
163
172
|
}
|
|
164
173
|
catch (error) {
|
|
165
174
|
scope.dispose();
|
|
166
175
|
throw error;
|
|
167
176
|
}
|
|
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
|
-
};
|
|
177
|
+
return { set: (value) => scope.set(value), dispose: () => scope.dispose() };
|
|
178
178
|
},
|
|
179
179
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
}
|
|
181
|
+
const markupDefinitions = new Map();
|
|
182
|
+
/** JSX factories preserve host identity by element type; source locations are metadata only. */
|
|
183
|
+
export function markup(tag, sources) {
|
|
184
|
+
let definition = markupDefinitions.get(tag);
|
|
185
|
+
if (!definition) {
|
|
186
|
+
definition = contentDefinition((scope, parent, before) => {
|
|
187
|
+
const node = element(parent, before, tag);
|
|
188
|
+
bindAttributes(scope, node, () => [scope.value.props], () => {
|
|
189
|
+
const { children: _children, ...attributes } = scope.value.props;
|
|
190
|
+
return attributes;
|
|
191
|
+
}, () => scope.value.sources);
|
|
192
|
+
text(scope, node, null, () => [scope.value.props.children], () => scope.value.props.children, () => scope.value.sources?.children);
|
|
193
|
+
});
|
|
194
|
+
markupDefinitions.set(tag, definition);
|
|
195
|
+
}
|
|
196
|
+
return (props) => new SlotPlacement(definition, { props, sources });
|
|
197
|
+
}
|
|
198
|
+
export function renderComponent(component, props) {
|
|
199
|
+
return component(props);
|
|
200
|
+
}
|
|
201
|
+
const listDefinition = contentDefinition((scope, parent, before) => {
|
|
202
|
+
each(scope, parent, before, () => scope.value.rows, () => [scope.value.render], true, (row, parent, before) => {
|
|
203
|
+
// The explicit list operation owns callback evaluation and keyed row lifetimes.
|
|
204
|
+
const update = () => scope.value.render(row.value[0], row.value[1]);
|
|
205
|
+
text(row, parent, before, () => [row.value, scope.value.render], update);
|
|
183
206
|
});
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
});
|
|
208
|
+
/** Preserve collection identity (or array reference identity) while rendering rows. */
|
|
209
|
+
export function list(rows, render) {
|
|
210
|
+
return new SlotPlacement(listDefinition, { rows, render });
|
|
186
211
|
}
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Define keyed row projection once. Every dependency is passed as an explicit input;
|
|
214
|
+
* only changed projected props enter the row view's renderer.
|
|
215
|
+
*/
|
|
216
|
+
export function listView(options) {
|
|
217
|
+
const equals = options.equals ?? Object.is;
|
|
218
|
+
const definition = contentDefinition((scope, parent, before) => {
|
|
219
|
+
each(scope, parent, before, () => scope.value.rows, () => [scope.value.input, scope.value.send], true, (row, parent, before) => {
|
|
220
|
+
const model = () => protectSnapshot(options.project(row.value[0], row.value[1], scope.value.input));
|
|
221
|
+
const child = new Scope(model(), (message) => scope.value.send(message), scope.report, scope.settlement);
|
|
222
|
+
row.cleanups.push(() => child.dispose());
|
|
223
|
+
options.view.build(child, parent, before);
|
|
224
|
+
row.jobs.push(() => {
|
|
225
|
+
const next = model();
|
|
226
|
+
if (!equals(child.value, next))
|
|
227
|
+
child.set(next);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
return (rows, input, ...dispatch) => new SlotPlacement(definition, { rows, input, send: dispatch[0] ?? unboundSend });
|
|
190
232
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
233
|
+
const observationDefinition = contentDefinition((scope, parent, before) => {
|
|
234
|
+
let unsubscribe = () => { };
|
|
235
|
+
let source = scope.value.source;
|
|
236
|
+
const child = new Scope(source.model(), unboundSend, scope.report, scope.settlement);
|
|
237
|
+
scope.cleanups.push(() => child.dispose(), () => unsubscribe());
|
|
238
|
+
text(child, parent, before, () => [child.value, scope.value.render], () => scope.value.render(child.value));
|
|
239
|
+
const connect = () => {
|
|
240
|
+
if (scope.disposed)
|
|
241
|
+
return;
|
|
242
|
+
const current = source;
|
|
243
|
+
const release = current.subscribe((value) => {
|
|
244
|
+
if (!scope.disposed && source === current)
|
|
245
|
+
child.set(value);
|
|
246
|
+
});
|
|
247
|
+
if (scope.disposed || source !== current)
|
|
248
|
+
release();
|
|
249
|
+
else {
|
|
250
|
+
unsubscribe = release;
|
|
251
|
+
child.set(current.model());
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
connect();
|
|
255
|
+
scope.jobs.push(() => {
|
|
256
|
+
if (source !== scope.value.source) {
|
|
257
|
+
unsubscribe();
|
|
258
|
+
source = scope.value.source;
|
|
259
|
+
connect();
|
|
260
|
+
}
|
|
261
|
+
else
|
|
262
|
+
child.set(source.model());
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
/** Render a declared source in its own subscribed region. No dependencies are inferred. */
|
|
266
|
+
export function observe(source, render) {
|
|
267
|
+
return new SlotPlacement(observationDefinition, { source, render });
|
|
195
268
|
}
|
|
196
269
|
/** Owned content in an element target, defaulting to the document body. */
|
|
197
270
|
export const Portal = /* @__PURE__ */ compiled((scope) => {
|
|
@@ -259,31 +332,55 @@ function buildFragment(parent) {
|
|
|
259
332
|
return fragment;
|
|
260
333
|
}
|
|
261
334
|
export function mountView(parent, definition, source, options = {}) {
|
|
335
|
+
return mountViewWithSettlement(parent, definition, source, { ...options, send: options.send ?? source.send ?? unboundSend }, new Settlement(options.runtime));
|
|
336
|
+
}
|
|
337
|
+
/** Internal mounting boundary. Its owner retains completion accounting if construction fails. */
|
|
338
|
+
export function mountViewWithSettlement(parent, definition, source, options, settlement) {
|
|
262
339
|
return commitDom(() => {
|
|
340
|
+
const initial = source.model();
|
|
263
341
|
const { start, end } = markers(parent, null);
|
|
264
|
-
const scope = new Scope(
|
|
342
|
+
const scope = new Scope(initial, options.send, options.onError, settlement);
|
|
265
343
|
let unsubscribe = () => { };
|
|
344
|
+
let ready = false;
|
|
345
|
+
let latest = scope.value;
|
|
266
346
|
try {
|
|
347
|
+
unsubscribe = source.subscribe((model) => {
|
|
348
|
+
latest = model;
|
|
349
|
+
if (ready)
|
|
350
|
+
scope.set(model);
|
|
351
|
+
});
|
|
267
352
|
const fragment = buildFragment(parent);
|
|
268
353
|
definition.build(scope, fragment, null);
|
|
269
354
|
parent.insertBefore(fragment, end);
|
|
270
|
-
|
|
355
|
+
ready = true;
|
|
356
|
+
if (!Object.is(latest, scope.value))
|
|
357
|
+
scope.set(latest);
|
|
271
358
|
}
|
|
272
359
|
catch (error) {
|
|
273
|
-
|
|
274
|
-
remove(start, end);
|
|
360
|
+
settlement.exit = Exit.die(error);
|
|
361
|
+
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
275
362
|
throw error;
|
|
276
363
|
}
|
|
364
|
+
let disposed = false;
|
|
277
365
|
const dispose = () => {
|
|
278
|
-
if (
|
|
366
|
+
if (disposed)
|
|
279
367
|
return;
|
|
280
|
-
|
|
368
|
+
disposed = true;
|
|
369
|
+
const finish = settlement.begin();
|
|
370
|
+
try {
|
|
371
|
+
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
finish();
|
|
375
|
+
}
|
|
281
376
|
};
|
|
282
377
|
return Object.assign(dispose, {
|
|
283
378
|
dispose,
|
|
284
379
|
close: () => {
|
|
285
|
-
|
|
286
|
-
|
|
380
|
+
return Effect.suspend(() => {
|
|
381
|
+
dispose();
|
|
382
|
+
return scope.settlement.wait();
|
|
383
|
+
});
|
|
287
384
|
},
|
|
288
385
|
});
|
|
289
386
|
});
|
|
@@ -385,7 +482,7 @@ export function bindAttribute(scope, element, name, dependencies, read, source)
|
|
|
385
482
|
let previous = dependencies();
|
|
386
483
|
let value = read();
|
|
387
484
|
attribute(element, name, value);
|
|
388
|
-
traceBinding(source, undefined, previous, 'binding');
|
|
485
|
+
traceBinding(bindingLocation(source), undefined, previous, 'binding');
|
|
389
486
|
if (!previous.length)
|
|
390
487
|
return;
|
|
391
488
|
// Keep the value cache in the dependency job itself, without an extra closure
|
|
@@ -399,7 +496,7 @@ export function bindAttribute(scope, element, name, dependencies, read, source)
|
|
|
399
496
|
attribute(element, name, nextValue);
|
|
400
497
|
value = nextValue;
|
|
401
498
|
}
|
|
402
|
-
traceBinding(source, previous, next, 'binding');
|
|
499
|
+
traceBinding(bindingLocation(source), previous, next, 'binding');
|
|
403
500
|
previous = next;
|
|
404
501
|
});
|
|
405
502
|
}
|
|
@@ -519,16 +616,20 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
519
616
|
let value = read();
|
|
520
617
|
if (Array.isArray(value)) {
|
|
521
618
|
validateContent(value);
|
|
522
|
-
value =
|
|
619
|
+
value = new SlotPlacement(arrayContent, value);
|
|
523
620
|
}
|
|
524
621
|
if (isContent(value)) {
|
|
525
|
-
node.data
|
|
622
|
+
if (node.data !== '')
|
|
623
|
+
node.data = '';
|
|
526
624
|
if (content?.definition === value.definition) {
|
|
527
625
|
content.mounted.set(value.value);
|
|
528
626
|
return;
|
|
529
627
|
}
|
|
530
|
-
content
|
|
628
|
+
const previous = content;
|
|
531
629
|
content = undefined;
|
|
630
|
+
previous?.mounted.dispose();
|
|
631
|
+
if (scope.disposed)
|
|
632
|
+
return;
|
|
532
633
|
if (start)
|
|
533
634
|
clear(start, node);
|
|
534
635
|
else {
|
|
@@ -536,10 +637,11 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
536
637
|
scope.cleanups.push(() => content?.mounted.dispose());
|
|
537
638
|
node.parentNode.insertBefore(start, node);
|
|
538
639
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
mounted
|
|
542
|
-
|
|
640
|
+
const mounted = value.definition.mount(node.parentNode, node, value.value, scope.report, scope.settlement);
|
|
641
|
+
if (scope.disposed)
|
|
642
|
+
mounted.dispose();
|
|
643
|
+
else
|
|
644
|
+
content = { definition: value.definition, mounted };
|
|
543
645
|
return;
|
|
544
646
|
}
|
|
545
647
|
if (value != null && typeof value === 'object')
|
|
@@ -575,20 +677,28 @@ const arrayContent = {
|
|
|
575
677
|
/** Runtime content arrays are positional; domain lists retain the explicit collection contract. */
|
|
576
678
|
function mountArray(report, before, initial, settlement) {
|
|
577
679
|
const cells = [];
|
|
680
|
+
let disposed = false;
|
|
578
681
|
const disposeCell = (cell) => {
|
|
579
682
|
cell.scope.dispose();
|
|
580
683
|
remove(cell.start, cell.end);
|
|
581
684
|
};
|
|
582
685
|
const set = (input) => {
|
|
686
|
+
if (disposed)
|
|
687
|
+
return;
|
|
583
688
|
const values = input;
|
|
584
689
|
validateContent(values);
|
|
585
|
-
while (cells.length > values.length)
|
|
690
|
+
while (cells.length > values.length) {
|
|
586
691
|
disposeCell(cells.pop());
|
|
692
|
+
if (disposed)
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
587
695
|
for (let index = 0; index < values.length; index++) {
|
|
588
696
|
const cell = cells[index];
|
|
589
697
|
if (cell) {
|
|
590
698
|
if (!Object.is(cell.scope.value, values[index]))
|
|
591
699
|
cell.scope.set(values[index]);
|
|
700
|
+
if (disposed)
|
|
701
|
+
return;
|
|
592
702
|
continue;
|
|
593
703
|
}
|
|
594
704
|
const scope = new Scope(values[index], unboundSend, report, settlement);
|
|
@@ -596,6 +706,10 @@ function mountArray(report, before, initial, settlement) {
|
|
|
596
706
|
const range = markers(fragment, null);
|
|
597
707
|
try {
|
|
598
708
|
text(scope, fragment, range.end, () => [scope.value], () => scope.value);
|
|
709
|
+
if (disposed) {
|
|
710
|
+
scope.dispose();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
599
713
|
before.parentNode.insertBefore(fragment, before);
|
|
600
714
|
cells.push({ scope, ...range });
|
|
601
715
|
}
|
|
@@ -606,6 +720,9 @@ function mountArray(report, before, initial, settlement) {
|
|
|
606
720
|
}
|
|
607
721
|
};
|
|
608
722
|
const dispose = () => {
|
|
723
|
+
if (disposed)
|
|
724
|
+
return;
|
|
725
|
+
disposed = true;
|
|
609
726
|
for (const cell of cells.splice(0))
|
|
610
727
|
disposeCell(cell);
|
|
611
728
|
};
|
|
@@ -652,29 +769,28 @@ export function event(scope, element, name, handler) {
|
|
|
652
769
|
effects?.dispose();
|
|
653
770
|
});
|
|
654
771
|
}
|
|
655
|
-
/**
|
|
772
|
+
/** The event binding owns its requests; snapshot renders may replace its callback. */
|
|
656
773
|
export function bindEvent(scope, element, name, dependencies, read) {
|
|
657
774
|
let current;
|
|
658
775
|
let owned;
|
|
659
776
|
scope.cleanups.push(() => owned?.dispose());
|
|
660
777
|
scope.watch(dependencies, () => {
|
|
661
778
|
const next = read();
|
|
662
|
-
if (next != null && typeof next !== 'function')
|
|
663
|
-
throw new TypeError(`Event ${name} must be a synchronous handler.`);
|
|
664
779
|
if (Object.is(current, next))
|
|
665
780
|
return;
|
|
666
|
-
owned?.dispose();
|
|
667
|
-
owned = undefined;
|
|
668
781
|
current = next;
|
|
669
|
-
if (
|
|
670
|
-
|
|
782
|
+
if (!next) {
|
|
783
|
+
owned?.dispose();
|
|
784
|
+
owned = undefined;
|
|
785
|
+
}
|
|
786
|
+
else if (!owned) {
|
|
671
787
|
owned = new Scope(next, scope.send, scope.report, scope.settlement);
|
|
672
|
-
event(owned, element, name,
|
|
788
|
+
event(owned, element, name, (event) => current?.(event));
|
|
673
789
|
}
|
|
674
790
|
});
|
|
675
791
|
}
|
|
676
792
|
/** Reconcile merged JSX attributes. Each host, event and control owns its cleanup. */
|
|
677
|
-
export function bindAttributes(scope, element, dependencies, read) {
|
|
793
|
+
export function bindAttributes(scope, element, dependencies, read, sources) {
|
|
678
794
|
const bindings = new Map();
|
|
679
795
|
let previous = {};
|
|
680
796
|
const isEvent = (name) => /^on[A-Z]/u.test(name);
|
|
@@ -689,8 +805,6 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
689
805
|
for (const name of Object.keys(next)) {
|
|
690
806
|
if (['key', 'ref', 'innerHTML', 'children'].includes(name))
|
|
691
807
|
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
808
|
}
|
|
695
809
|
for (const name of Object.keys(previous)) {
|
|
696
810
|
if (Object.hasOwn(next, name))
|
|
@@ -701,8 +815,11 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
701
815
|
attribute(element, name, undefined);
|
|
702
816
|
}
|
|
703
817
|
for (const [name, value] of Object.entries(next)) {
|
|
818
|
+
if (!Object.hasOwn(previous, name) || !Object.is(previous[name], value))
|
|
819
|
+
traceBinding(sources?.()?.[name], Object.hasOwn(previous, name) ? [previous[name]] : undefined, [value], 'binding');
|
|
704
820
|
if (name !== 'use' && !isEvent(name) && !isControl(name)) {
|
|
705
|
-
|
|
821
|
+
if (!Object.hasOwn(previous, name) || !Object.is(previous[name], value))
|
|
822
|
+
attribute(element, name, value);
|
|
706
823
|
continue;
|
|
707
824
|
}
|
|
708
825
|
let binding = bindings.get(name);
|
|
@@ -767,6 +884,8 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
767
884
|
let previousOuter = [];
|
|
768
885
|
let previousIdentities = [];
|
|
769
886
|
const update = () => {
|
|
887
|
+
if (scope.disposed)
|
|
888
|
+
return;
|
|
770
889
|
const target = end.parentNode;
|
|
771
890
|
const next = read();
|
|
772
891
|
const nextOuter = outer();
|
|
@@ -787,8 +906,11 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
787
906
|
target instanceof Element &&
|
|
788
907
|
target.firstChild === rows.get(previousIdentities[0])?.start &&
|
|
789
908
|
target.lastChild === end) {
|
|
790
|
-
for (const row of rows.values())
|
|
909
|
+
for (const row of rows.values()) {
|
|
791
910
|
row.scope.dispose();
|
|
911
|
+
if (scope.disposed)
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
792
914
|
target.replaceChildren(end);
|
|
793
915
|
rows.clear();
|
|
794
916
|
previousList = next;
|
|
@@ -796,24 +918,21 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
796
918
|
previousIdentities = [];
|
|
797
919
|
return;
|
|
798
920
|
}
|
|
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
|
-
});
|
|
921
|
+
const identities = validateIdentities(items, (item, index) => collection ? collection.identity(item, index) : item);
|
|
805
922
|
const keep = new Set(identities);
|
|
806
923
|
for (const [key, row] of rows) {
|
|
807
924
|
if (keep.has(key))
|
|
808
925
|
continue;
|
|
809
926
|
row.scope.dispose();
|
|
927
|
+
if (scope.disposed)
|
|
928
|
+
return;
|
|
810
929
|
remove(row.start, row.end);
|
|
811
930
|
rows.delete(key);
|
|
812
931
|
}
|
|
813
932
|
for (let index = 0; index < items.length; index++) {
|
|
814
933
|
const key = identities[index];
|
|
815
934
|
const row = rows.get(key);
|
|
816
|
-
const item =
|
|
935
|
+
const item = items[index];
|
|
817
936
|
if (!row) {
|
|
818
937
|
const fragment = buildFragment(target);
|
|
819
938
|
const range = markers(fragment, null);
|
|
@@ -825,6 +944,10 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
825
944
|
child.dispose();
|
|
826
945
|
throw error;
|
|
827
946
|
}
|
|
947
|
+
if (scope.disposed) {
|
|
948
|
+
child.dispose();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
828
951
|
// A single element is its own stable range. Keep markers for dynamic/multiple roots.
|
|
829
952
|
const root = range.start.nextSibling;
|
|
830
953
|
const single = root instanceof Element && root.nextSibling === range.end;
|
|
@@ -843,6 +966,8 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
843
966
|
row.scope.value[0] !== item ||
|
|
844
967
|
(indexUsed && row.scope.value[1] !== index)) {
|
|
845
968
|
row.scope.set([item, index]);
|
|
969
|
+
if (scope.disposed)
|
|
970
|
+
return;
|
|
846
971
|
}
|
|
847
972
|
}
|
|
848
973
|
// Retained prefixes are already ordered. New tail rows were appended above and
|
|
@@ -892,16 +1017,6 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
892
1017
|
});
|
|
893
1018
|
update();
|
|
894
1019
|
}
|
|
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
1020
|
export function child(scope, parent, before, definition, dependencies, model, send) {
|
|
906
1021
|
const child = new Scope(model(), send, scope.report, scope.settlement);
|
|
907
1022
|
scope.cleanups.push(() => child.dispose());
|
|
@@ -965,7 +1080,7 @@ export function attach(scope, element, dependencies, read) {
|
|
|
965
1080
|
let active;
|
|
966
1081
|
scope.cleanups.push(() => {
|
|
967
1082
|
generation++;
|
|
968
|
-
active?.dispose();
|
|
1083
|
+
active?.dispose(scope.settlement.exit);
|
|
969
1084
|
active = undefined;
|
|
970
1085
|
});
|
|
971
1086
|
scope.watch(dependencies, () => {
|
|
@@ -981,12 +1096,14 @@ export function attach(scope, element, dependencies, read) {
|
|
|
981
1096
|
if (!scope.disposed && token === generation) {
|
|
982
1097
|
const finished = scope.settlement.begin();
|
|
983
1098
|
try {
|
|
984
|
-
const acquired =
|
|
985
|
-
acquired.closed.
|
|
1099
|
+
const acquired = prepareMount(element, mount, scope.report, scope.settlement.runtime);
|
|
1100
|
+
Effect.runFork(acquired.closed).addObserver(finished);
|
|
986
1101
|
if (scope.disposed || token !== generation)
|
|
987
1102
|
acquired.dispose();
|
|
988
|
-
else
|
|
1103
|
+
else {
|
|
989
1104
|
active = acquired;
|
|
1105
|
+
acquired.start();
|
|
1106
|
+
}
|
|
990
1107
|
}
|
|
991
1108
|
catch (error) {
|
|
992
1109
|
finished();
|
|
@@ -1106,4 +1223,3 @@ export function template(build, depth = 0) {
|
|
|
1106
1223
|
export function literal(parent, before, value) {
|
|
1107
1224
|
parent.insertBefore((parent.ownerDocument ?? document).createTextNode(value), before);
|
|
1108
1225
|
}
|
|
1109
|
-
export { intrinsic } from './intrinsic.js';
|
package/dist/effectEvent.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import type * as Scope from 'effect/Scope';
|
|
2
3
|
import { type ReportError } from './errors.js';
|
|
3
4
|
import type { Settlement } from './settlement.js';
|
|
4
5
|
import { type UiRuntime } from './runtime.js';
|
|
5
6
|
declare const tag: unique symbol;
|
|
6
7
|
export interface EffectEventRequest {
|
|
7
8
|
readonly [tag]: true;
|
|
8
|
-
readonly policy: 'drop' | 'replace';
|
|
9
|
-
readonly effect: Effect.Effect<unknown, unknown>;
|
|
9
|
+
readonly policy: 'drop' | 'replace' | 'parallel';
|
|
10
|
+
readonly effect: Effect.Effect<unknown, unknown, Scope.Scope>;
|
|
10
11
|
}
|
|
11
12
|
/** Capture/prevent native events on every call; the policy gates only Effect execution. */
|
|
12
|
-
export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E>): (event: EventType) => EffectEventRequest;
|
|
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
|
-
/**
|
|
13
|
+
export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E, Scope.Scope>): (event: EventType) => EffectEventRequest;
|
|
14
|
+
export declare function effectEvent<EventType extends Event, E, R>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E, R | Scope.Scope>, runtime: UiRuntime<R>): (event: EventType) => EffectEventRequest;
|
|
15
|
+
/** Capture an operation's services in Effect setup; its event listener supplies the work scope. */
|
|
16
|
+
export declare const makeEffectHandler: <Args extends readonly unknown[], A, E, R = never>(work: (...args: Args) => Effect.Effect<A, E, R | Scope.Scope>) => Effect.Effect<(...args: Args) => Effect.Effect<A, E, Scope.Scope>, never, R>;
|
|
17
|
+
/** Internal listener owner, allocated only when an event returns an owned request. */
|
|
15
18
|
export declare function eventEffects(report: ReportError, settlement?: Settlement): {
|
|
16
|
-
accept(value: unknown): void;
|
|
19
|
+
accept(value: EffectEventRequest | Effect.Effect<unknown, unknown, Scope.Scope>): void;
|
|
17
20
|
dispose(): void;
|
|
18
21
|
};
|
|
19
22
|
export {};
|