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.
Files changed (59) hide show
  1. package/README.md +35 -4
  2. package/dist/AsyncContent.d.ts +1 -1
  3. package/dist/AsyncContent.js +18 -35
  4. package/dist/boundary.d.ts +11 -0
  5. package/dist/boundary.js +66 -0
  6. package/dist/cache-internals.d.ts +4 -0
  7. package/dist/cache.d.ts +21 -5
  8. package/dist/cache.js +178 -25
  9. package/dist/collection.d.ts +1 -1
  10. package/dist/collection.js +1 -3
  11. package/dist/component.d.ts +3 -0
  12. package/dist/component.js +73 -10
  13. package/dist/dom.d.ts +35 -24
  14. package/dist/dom.js +177 -128
  15. package/dist/effectEvent.d.ts +5 -4
  16. package/dist/effectEvent.js +29 -38
  17. package/dist/form.d.ts +2 -1
  18. package/dist/http.d.ts +2 -1
  19. package/dist/http.js +1 -1
  20. package/dist/index.d.ts +7 -4
  21. package/dist/index.js +6 -3
  22. package/dist/infinite-query.d.ts +42 -0
  23. package/dist/infinite-query.js +150 -0
  24. package/dist/jsx-runtime.d.ts +6 -0
  25. package/dist/jsx-runtime.js +14 -1
  26. package/dist/jsx.d.ts +2 -1
  27. package/dist/keyed-tasks.d.ts +30 -0
  28. package/dist/keyed-tasks.js +102 -0
  29. package/dist/lazy.d.ts +2 -1
  30. package/dist/lazy.js +20 -7
  31. package/dist/load.d.ts +1 -1
  32. package/dist/load.js +1 -1
  33. package/dist/mount.d.ts +13 -2
  34. package/dist/mount.js +82 -21
  35. package/dist/owner.d.ts +9 -5
  36. package/dist/owner.js +27 -9
  37. package/dist/pages.d.ts +16 -15
  38. package/dist/pages.js +8 -5
  39. package/dist/program.d.ts +14 -2
  40. package/dist/program.js +96 -36
  41. package/dist/query-internals.d.ts +5 -3
  42. package/dist/query.d.ts +7 -1
  43. package/dist/query.js +7 -3
  44. package/dist/resource.js +3 -1
  45. package/dist/runtime.d.ts +2 -1
  46. package/dist/runtime.js +3 -1
  47. package/dist/session.js +23 -7
  48. package/dist/settlement.d.ts +8 -0
  49. package/dist/settlement.js +32 -0
  50. package/dist/sharing.js +16 -13
  51. package/dist/snapshot.d.ts +1 -1
  52. package/dist/snapshot.js +31 -25
  53. package/dist/task.d.ts +1 -1
  54. package/dist/task.js +3 -1
  55. package/dist/tasks.d.ts +1 -1
  56. package/dist/tasks.js +3 -1
  57. package/dist/testing.d.ts +5 -4
  58. package/dist/testing.js +4 -2
  59. 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 { startMount } from './mount.js';
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
- revision = 0;
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
- /** Compiler marker for mounting a view with an explicit model and message dispatcher. */
108
- export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(_props) {
109
- throw new Error('ViewBinding reached runtime without the EffectWeb JSX compiler');
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
- /** Compiler marker: declare inside view(), or directly in a compiled component prop. */
117
- export function slot(_render) {
118
- throw new Error('EffectWeb slot reached runtime without the EffectWeb JSX compiler');
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
- /** Compiler output. Captures follow their declaring scope; placements own independent scopes. */
136
- export function compiledSlot(owner, build) {
137
- const placements = new Set();
138
- owner.jobs.push(() => {
139
- for (const scope of placements)
140
- scope.set(scope.value);
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
- const definition = {
148
- mount(parent, before, value) {
149
- if (owner.disposed)
150
- throw new Error('Cannot mount content after its declaring view disposed');
151
- const scope = new Scope(value, owner.send, owner.report);
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
- parent.insertBefore(fragment, before);
158
- placements.add(scope);
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
- /** This declaration is a compiler marker, never a component setup callback. */
182
- export function view(_render) {
183
- throw new Error('EffectWeb view reached runtime without the EffectWeb JSX compiler');
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
- export function compiled(build) {
186
- return Object.assign(() => {
187
- throw new Error('Mount compiled views with mountView or inside another compiled view');
188
- }, { build, [jsxComponent]: true });
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
- unsubscribe = source.subscribe((model) => scope.set(model));
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
- return () => {
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 = { [contentBrand]: true, definition: arrayContent, 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?.mounted.dispose();
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
- content = {
527
- definition: value.definition,
528
- mounted: value.definition.mount(node.parentNode, node, value.value, scope.report),
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
- /** One handler identity owns its requests, regardless of the JSX attribute spelling. */
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 (typeof next === 'function') {
657
- const handler = next;
658
- owned = new Scope(next, scope.send, scope.report);
659
- event(owned, element, name, handler);
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
- attribute(element, name, value);
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 = row ? shareData(row.scope.value[0], items[index]) : items[index];
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
- scope.jobs.push(() => active?.set(scope.value));
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 = startMount(element, mount, scope.report);
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;
@@ -1,5 +1,6 @@
1
- import { Effect } from 'effect';
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 a value needing inspection. */
14
- export declare function eventEffects(report: ReportError): {
15
- accept(value: unknown): void;
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 {};