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.
Files changed (58) 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/cache.d.ts +2 -2
  5. package/dist/cache.js +7 -3
  6. package/dist/collection.d.ts +1 -1
  7. package/dist/collection.js +1 -3
  8. package/dist/component.js +5 -2
  9. package/dist/dom.d.ts +26 -23
  10. package/dist/dom.js +150 -119
  11. package/dist/effectEvent.d.ts +3 -3
  12. package/dist/effectEvent.js +27 -39
  13. package/dist/form.d.ts +2 -1
  14. package/dist/http.d.ts +2 -1
  15. package/dist/http.js +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/infinite-query.d.ts +1 -1
  19. package/dist/infinite-query.js +1 -1
  20. package/dist/jsx-runtime.d.ts +6 -0
  21. package/dist/jsx-runtime.js +14 -1
  22. package/dist/jsx.d.ts +2 -1
  23. package/dist/keyed-tasks.d.ts +5 -4
  24. package/dist/keyed-tasks.js +27 -22
  25. package/dist/lazy.d.ts +2 -1
  26. package/dist/lazy.js +13 -8
  27. package/dist/load.d.ts +1 -1
  28. package/dist/load.js +1 -1
  29. package/dist/mount.d.ts +12 -3
  30. package/dist/mount.js +63 -20
  31. package/dist/owner.d.ts +8 -6
  32. package/dist/owner.js +22 -40
  33. package/dist/pages.d.ts +1 -1
  34. package/dist/pages.js +2 -1
  35. package/dist/program.d.ts +7 -5
  36. package/dist/program.js +33 -20
  37. package/dist/query-internals.d.ts +1 -1
  38. package/dist/query.d.ts +1 -1
  39. package/dist/query.js +0 -2
  40. package/dist/resource.js +3 -1
  41. package/dist/runtime.d.ts +2 -1
  42. package/dist/runtime.js +3 -1
  43. package/dist/session.js +10 -3
  44. package/dist/settlement.d.ts +2 -1
  45. package/dist/settlement.js +13 -8
  46. package/dist/snapshot.d.ts +1 -1
  47. package/dist/snapshot.js +10 -12
  48. package/dist/task.d.ts +1 -1
  49. package/dist/task.js +3 -1
  50. package/dist/tasks.d.ts +1 -1
  51. package/dist/tasks.js +2 -1
  52. package/dist/testing.d.ts +5 -4
  53. package/dist/testing.js +4 -2
  54. package/package.json +1 -1
  55. package/dist/intrinsic.d.ts +0 -2
  56. package/dist/intrinsic.js +0 -120
  57. package/dist/slotIdentity.d.ts +0 -1
  58. 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 { startMount } from './mount.js';
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
- /** Compiler marker for mounting a view with an explicit model and message dispatcher. */
112
- export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(_props) {
113
- 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 });
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
- /** Compiler marker: declare inside view(), or directly in a compiled component prop. */
121
- export function slot(_render) {
122
- throw new Error('EffectWeb slot reached runtime without the EffectWeb JSX compiler');
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
- /** Compiler output. Captures follow their declaring scope; placements own independent scopes. */
140
- export function compiledSlot(owner, build) {
141
- const placements = new Set();
142
- owner.jobs.push(() => {
143
- for (const scope of placements)
144
- scope.set(scope.value);
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
- const definition = {
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
- if (owner.disposed)
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
- parent.insertBefore(fragment, before);
162
- placements.add(scope);
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
- /** This declaration is a compiler marker, never a component setup callback. */
188
- export function view(_render) {
189
- 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);
190
186
  }
191
- export function compiled(build) {
192
- return Object.assign(() => {
193
- throw new Error('Mount compiled views with mountView or inside another compiled view');
194
- }, { 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 });
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
- unsubscribe = source.subscribe((model) => scope.set(model));
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
- dispose();
286
- return scope.settlement.wait();
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 = { [contentBrand]: true, definition: arrayContent, 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?.mounted.dispose();
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
- content = {
540
- definition: value.definition,
541
- mounted: value.definition.mount(node.parentNode, node, value.value, scope.report, scope.settlement),
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
- /** 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. */
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 (typeof next === 'function') {
670
- const handler = next;
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, handler);
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
- attribute(element, name, value);
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 = row ? shareData(row.scope.value[0], items[index]) : items[index];
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 = startMount(element, mount, scope.report);
985
- acquired.closed.then(finished, finished);
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';
@@ -1,4 +1,4 @@
1
- import { Effect } from 'effect';
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 a value needing inspection. */
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(value: unknown): void;
16
+ accept(request: EffectEventRequest): void;
17
17
  dispose(): void;
18
18
  };
19
19
  export {};
@@ -1,4 +1,6 @@
1
- import { Effect, Fiber } from 'effect';
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 a value needing inspection. */
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(value) {
22
+ accept(request) {
21
23
  if (disposed)
22
24
  return;
23
- if (value && typeof value === 'object' && tag in value) {
24
- const request = value;
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
- fiber.addObserver(() => finished?.());
34
- token.fiber = fiber;
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
- if (exit._tag === 'Failure')
44
- reportSafely(report, exit.cause);
45
- });
46
- }
47
- catch (error) {
38
+ if (exit._tag === 'Failure' && (current || !Cause.hasInterruptsOnly(exit.cause)))
39
+ reportSafely(report, exit.cause);
48
40
  finished?.();
49
- if (active === token)
50
- active = undefined;
51
- reportSafely(report, error);
41
+ });
42
+ if (disposed || active !== token) {
43
+ Effect.runFork(Fiber.interrupt(fiber));
52
44
  }
53
45
  }
54
- else if (Effect.isEffect(value)) {
55
- reportSafely(report, new Error('Event returned an unowned Effect. Use effectEvent(policy, factory) or dispatch a command.'));
56
- }
57
- else if (value &&
58
- typeof value === 'object' &&
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 { Cause, Effect } from 'effect';
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 { Effect, type Schema } from 'effect';
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 { Effect } from 'effect';
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. */