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/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { patchModel } from './state.js';
6
6
  export { defineActions, type ActionMessage } from './actions.js';
7
7
  export { taskComponent, taskControls, type TaskModel, type TaskMessage } from './task.js';
8
8
  export { effectEvent } from './effectEvent.js';
9
- export { mountView, view, ViewBinding, type View } from './dom.js';
9
+ export { mountView, view, list, ViewBinding, type View } from './dom.js';
10
10
  export { lazyView, type LazyViewOptions } from './lazy.js';
11
11
  export type { JSX } from './jsx.js';
12
12
  export { Portal, domBinding, domMount, type DomMount, type PortalProps } from './mount.js';
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { patchModel } from './state.js';
5
5
  export { defineActions } from './actions.js';
6
6
  export { taskComponent, taskControls } from './task.js';
7
7
  export { effectEvent } from './effectEvent.js';
8
- export { mountView, view, ViewBinding } from './dom.js';
8
+ export { mountView, view, list, ViewBinding } from './dom.js';
9
9
  export { lazyView } from './lazy.js';
10
10
  export { Portal, domBinding, domMount } from './mount.js';
11
11
  export { pages, } from './pages.js';
@@ -1,4 +1,4 @@
1
- import { Effect } from 'effect';
1
+ import * as Effect from 'effect/Effect';
2
2
  import { type Query, type QueryArgs, type QueryGroup } from './query.js';
3
3
  import type { QueryCache } from './cache.js';
4
4
  import { type QueryResource } from './session.js';
@@ -1,4 +1,4 @@
1
- import { Effect } from 'effect';
1
+ import * as Effect from 'effect/Effect';
2
2
  import { query, encodeQueryKey, } from './query.js';
3
3
  import { cacheInternals } from './cache-internals.js';
4
4
  import { queryResource } from './session.js';
@@ -1 +1,7 @@
1
1
  export type { JSX } from './jsx.js';
2
+ import type { JSX } from './jsx.js';
3
+ export declare function Fragment(props: {
4
+ readonly children?: JSX.Element;
5
+ }): JSX.Element;
6
+ export declare function jsx(type: string | ((props: never) => JSX.Element), props: Readonly<Record<string, unknown>>): JSX.Element;
7
+ export { jsx as jsxs, jsx as jsxDEV };
@@ -1 +1,14 @@
1
- export {};
1
+ import { markup } from './dom.js';
2
+ export function Fragment(props) {
3
+ return props.children;
4
+ }
5
+ const tags = new Map();
6
+ export function jsx(type, props) {
7
+ if (typeof type !== 'string')
8
+ return type(props);
9
+ let render = tags.get(type);
10
+ if (!render)
11
+ tags.set(type, (render = markup(type)));
12
+ return render(props);
13
+ }
14
+ export { jsx as jsxs, jsx as jsxDEV };
package/dist/jsx.d.ts CHANGED
@@ -16,10 +16,11 @@ export declare namespace JSX {
16
16
  export interface ElementArray extends ReadonlyArray<Element>, SnapshotOpaque {
17
17
  }
18
18
  export type Element = string | number | boolean | null | undefined | CompiledContent | ElementArray;
19
+ export type EventResult = void | boolean | EffectEventRequest;
19
20
  export type EventHandler<T, E extends Event> = (event: E & {
20
21
  currentTarget: T;
21
22
  target: EventTarget & globalThis.Element;
22
- }) => void | boolean | EffectEventRequest;
23
+ }) => EventResult;
23
24
  export type EventHandlerUnion<T, E extends Event> = EventHandler<T, E>;
24
25
  export type CSSProperties = {
25
26
  [property: string]: string | number | undefined | null;
@@ -1,4 +1,5 @@
1
- import { Cause, Effect } from 'effect';
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Effect from 'effect/Effect';
2
3
  import { type TaskPolicy } from './program.js';
3
4
  import type { DisposableOwner } from './owner.js';
4
5
  import { type UiRuntime } from './runtime.js';
@@ -13,7 +14,7 @@ export type TaskOutcome<A, E> = {
13
14
  readonly _tag: 'Cancelled' | 'Superseded' | 'Dropped';
14
15
  };
15
16
  export interface TaskHandle<A, E> {
16
- readonly outcome: Promise<TaskOutcome<A, E>>;
17
+ readonly outcome: Effect.Effect<TaskOutcome<A, E>>;
17
18
  }
18
19
  /** Per-entity serialization. Inputs are captured now; run reads services when work starts. */
19
20
  export declare function keyedTasks<Key, Input, A, E = never, R = never>(owner: DisposableOwner, definition: {
@@ -23,7 +24,7 @@ export declare function keyedTasks<Key, Input, A, E = never, R = never>(owner: D
23
24
  }, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): {
24
25
  submit(key: Key, input: Input | Snapshot<Input>): TaskHandle<A, E>;
25
26
  cancel(key: Key): void;
26
- drain(...selected: [] | [Key]): Promise<void>;
27
- close: () => Promise<void>;
27
+ drain(...selected: [] | [Key]): Effect.Effect<void>;
28
+ close: () => Effect.Effect<void, never, never>;
28
29
  dispose: () => void;
29
30
  };
@@ -1,4 +1,7 @@
1
- import { Cause, Effect, Exit } from 'effect';
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Deferred from 'effect/Deferred';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Exit from 'effect/Exit';
2
5
  import { commandSlot, program } from './program.js';
3
6
  import { defaultUiRuntime } from './runtime.js';
4
7
  import { reportError, reportSafely } from './errors.js';
@@ -27,39 +30,42 @@ export function keyedTasks(owner, definition, ...provided) {
27
30
  for (const entry of entries.values())
28
31
  entry.source.dispose();
29
32
  };
30
- const close = async () => {
33
+ const close = () => Effect.gen(function* () {
31
34
  dispose();
32
- await Promise.all([...entries.values()].map((entry) => entry.source.close()));
35
+ yield* Effect.all([...entries.values()].map((entry) => entry.source.close()), { concurrency: 'unbounded' });
33
36
  entries.clear();
34
- };
37
+ });
35
38
  owner.own({ dispose, close });
36
39
  return {
37
40
  submit(key, input) {
38
41
  if (disposed || owner.disposed)
39
- return { outcome: Promise.resolve(Object.freeze({ _tag: 'Cancelled' })) };
42
+ return { outcome: Effect.succeed(Object.freeze({ _tag: 'Cancelled' })) };
40
43
  const captured = protectSnapshot(input);
41
44
  const entry = get(key);
42
45
  entry.version++;
43
- let settle;
44
- const outcome = new Promise((resolve) => {
45
- settle = (result) => resolve(protectSnapshot(result));
46
- });
47
- outcome
48
- .then(async () => {
46
+ const result = Deferred.makeUnsafe();
47
+ const outcome = Deferred.await(result);
48
+ const settle = (value) => {
49
+ Deferred.doneUnsafe(result, Effect.succeed(protectSnapshot(value)));
50
+ };
51
+ Effect.runFork(Effect.gen(function* () {
52
+ yield* outcome;
49
53
  if (entry.retiring)
50
54
  return;
51
55
  entry.retiring = true;
52
56
  let version;
53
57
  do {
54
58
  version = entry.version;
55
- await entry.source.awaitStopped();
59
+ yield* entry.source.awaitStopped();
56
60
  } while (version !== entry.version);
57
61
  if (entries.get(key) === entry) {
58
62
  entries.delete(key);
59
63
  entry.source.dispose();
60
64
  }
61
- })
62
- .catch((error) => reportSafely(reportError, error));
65
+ })).addObserver((exit) => {
66
+ if (Exit.isFailure(exit))
67
+ reportSafely(reportError, exit.cause);
68
+ });
63
69
  const action = Effect.uninterruptibleMask((restore) => restore(Effect.suspend(() => definition.run(key, captured)).pipe(Effect.map((value) => protectSnapshot(value)))).pipe(Effect.exit, Effect.flatMap((exit) => Effect.sync(() => settle(Exit.isSuccess(exit)
64
70
  ? { _tag: 'Success', value: exit.value }
65
71
  : Cause.hasInterruptsOnly(exit.cause)
@@ -82,14 +88,13 @@ export function keyedTasks(owner, definition, ...provided) {
82
88
  entry.source.send({ cancel: true });
83
89
  }
84
90
  },
85
- async drain(...selected) {
86
- const pending = selected.length
87
- ? [entries.get(selected[0])].filter((entry) => !!entry)
88
- : [...entries.values()];
89
- await Promise.all(pending.map(async (entry) => {
90
- await entry.source.awaitIdle();
91
- await entry.source.awaitStopped();
92
- }));
91
+ drain(...selected) {
92
+ return Effect.suspend(() => {
93
+ const pending = selected.length
94
+ ? [entries.get(selected[0])].filter((entry) => !!entry)
95
+ : [...entries.values()];
96
+ return Effect.all(pending.map((entry) => Effect.andThen(entry.source.awaitIdle(), entry.source.awaitStopped())), { concurrency: 'unbounded', discard: true });
97
+ });
93
98
  },
94
99
  close,
95
100
  dispose,
package/dist/lazy.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Effect, type Cause } from 'effect';
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Effect from 'effect/Effect';
2
3
  import { type View } from './dom.js';
3
4
  import { type UiRuntime } from './runtime.js';
4
5
  export interface LazyViewOptions<Model, Message, E> {
package/dist/lazy.js CHANGED
@@ -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 { child, compiled, viewRegion } from './dom.js';
3
5
  import { reportSafely } from './errors.js';
4
6
  import { defaultUiRuntime } from './runtime.js';
@@ -28,20 +30,18 @@ export function lazyView(load, ...provided) {
28
30
  const finished = scope.settlement.begin();
29
31
  try {
30
32
  fiber = Effect.runFork(runtime.provide(Effect.suspend(load)));
31
- fiber.addObserver(finished);
32
33
  }
33
34
  catch (error) {
34
35
  finished();
35
36
  throw error;
36
37
  }
37
- if (scope.disposed) {
38
- interrupt();
39
- return;
40
- }
41
38
  fiber.addObserver((exit) => {
42
- if (scope.disposed)
43
- return;
44
39
  try {
40
+ if (scope.disposed) {
41
+ if (exit._tag === 'Failure' && !Cause.hasInterruptsOnly(exit.cause))
42
+ reportSafely(scope.report, exit.cause);
43
+ return;
44
+ }
45
45
  if (exit._tag === 'Success') {
46
46
  loaded = exit.value;
47
47
  render(loaded);
@@ -61,6 +61,11 @@ export function lazyView(load, ...provided) {
61
61
  catch (error) {
62
62
  reportSafely(scope.report, error);
63
63
  }
64
+ finally {
65
+ finished();
66
+ }
64
67
  });
68
+ if (scope.disposed)
69
+ interrupt();
65
70
  });
66
71
  }
package/dist/load.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect } from 'effect';
1
+ import * as Effect from 'effect/Effect';
2
2
  export type UiLoad<A, E = unknown, R = never> = Effect.Effect<A, E, R>;
3
3
  export declare const loadEffect: <A, E, R>(load: () => UiLoad<A, E, R>) => UiLoad<A, E, R>;
4
4
  /** Adapt Promise libraries at the integration boundary. Pass the signal to APIs that support it. */
package/dist/load.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect } from 'effect';
1
+ import * as Effect from 'effect/Effect';
2
2
  export const loadEffect = (load) => Effect.suspend(load);
3
3
  /** Adapt Promise libraries at the integration boundary. Pass the signal to APIs that support it. */
4
4
  export const fromPromise = (load) => Effect.tryPromise({ try: load, catch: (error) => error });
package/dist/mount.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
2
  import { type ReportError } from './errors.js';
3
- import { Effect } from 'effect';
3
+ import * as Effect from 'effect/Effect';
4
4
  import { type UiRuntime } from './runtime.js';
5
5
  export { Portal, type PortalProps } from './dom.js';
6
6
  type Work<R = never> = (() => void) | {
@@ -16,9 +16,18 @@ export interface DomMount<T extends Element = Element> {
16
16
  export declare function domMount<T extends Element, R = never>(start: (element: T) => Work<R>, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): DomMount<T>;
17
17
  /** A stable DOM lifecycle with fresh immutable inputs, for focus, measurements and native events. */
18
18
  export declare function domBinding<T extends Element, A, R = never>(data: A | Snapshot<A>, acquire: (element: T, input: () => Snapshot<A>) => Work<R>, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): DomMount<T>;
19
+ /** Allocate the lifetime before acquisition so reentrant publications can update or close it. */
20
+ export declare function prepareMount<T extends Element>(element: T, mount: DomMount<T>, report?: ReportError): {
21
+ closed: Effect.Effect<void, never, never>;
22
+ start(): void;
23
+ close: () => Effect.Effect<void, never, never>;
24
+ update(next: DomMount<T>): boolean;
25
+ dispose: () => void;
26
+ };
19
27
  export declare function startMount<T extends Element>(element: T, mount: DomMount<T>, report?: ReportError): {
20
- closed: Promise<void>;
21
- close(): Promise<void>;
28
+ closed: Effect.Effect<void, never, never>;
29
+ start(): void;
30
+ close: () => Effect.Effect<void, never, never>;
22
31
  update(next: DomMount<T>): boolean;
23
32
  dispose: () => void;
24
33
  };
package/dist/mount.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { reportError, reportSafely } from './errors.js';
2
- import { Effect, Fiber } from 'effect';
2
+ import * as Cause from 'effect/Cause';
3
+ import * as Deferred from 'effect/Deferred';
4
+ import * as Effect from 'effect/Effect';
5
+ import * as Fiber from 'effect/Fiber';
3
6
  import { defaultUiRuntime } from './runtime.js';
4
7
  export { Portal } from './dom.js';
5
8
  /** Element-owned work. Changed acquisition functions replace and interrupt the prior lifetime. */
@@ -26,24 +29,22 @@ export function domBinding(data, acquire, ...provided) {
26
29
  },
27
30
  };
28
31
  }
29
- export function startMount(element, mount, report = reportError) {
32
+ /** Allocate the lifetime before acquisition so reentrant publications can update or close it. */
33
+ export function prepareMount(element, mount, report = reportError) {
30
34
  let data = mount.data;
31
- const work = mount.acquire(element, () => data);
32
- const fiber = Effect.isEffect(work) ? Effect.runFork(work) : undefined;
35
+ let work;
36
+ let fiber;
37
+ let started = false;
33
38
  let disposed = false;
34
- let finish;
35
- const closed = new Promise((resolve) => {
36
- finish = resolve;
37
- });
38
- fiber?.addObserver((exit) => {
39
- if (exit._tag === 'Failure' && !disposed)
40
- reportSafely(report, exit.cause);
41
- finish();
42
- });
43
- const dispose = () => {
44
- if (disposed)
39
+ let released = false;
40
+ let changedDuringSetup = false;
41
+ const completed = Deferred.makeUnsafe();
42
+ const closed = Deferred.await(completed);
43
+ const finish = () => Deferred.doneUnsafe(completed, Effect.void);
44
+ const release = () => {
45
+ if (released || work === undefined || (Effect.isEffect(work) && !fiber))
45
46
  return;
46
- disposed = true;
47
+ released = true;
47
48
  if (fiber)
48
49
  Effect.runFork(Fiber.interrupt(fiber));
49
50
  else {
@@ -58,20 +59,62 @@ export function startMount(element, mount, report = reportError) {
58
59
  }
59
60
  }
60
61
  };
62
+ const dispose = () => {
63
+ if (disposed)
64
+ return;
65
+ disposed = true;
66
+ if (!started)
67
+ finish();
68
+ else
69
+ release();
70
+ };
61
71
  return {
62
72
  closed,
63
- close() {
73
+ start() {
74
+ if (started || disposed)
75
+ return;
76
+ started = true;
77
+ try {
78
+ work = mount.acquire(element, () => data);
79
+ if (Effect.isEffect(work)) {
80
+ fiber = Effect.runFork(work);
81
+ fiber.addObserver((exit) => {
82
+ if (exit._tag === 'Failure' && (!disposed || !Cause.hasInterruptsOnly(exit.cause)))
83
+ reportSafely(report, exit.cause);
84
+ finish();
85
+ });
86
+ }
87
+ if (disposed)
88
+ release();
89
+ else if (changedDuringSetup && !fiber && typeof work === 'object' && 'update' in work)
90
+ work.update?.();
91
+ }
92
+ catch (error) {
93
+ dispose();
94
+ if (work === undefined)
95
+ finish();
96
+ throw error;
97
+ }
98
+ },
99
+ close: () => Effect.suspend(() => {
64
100
  dispose();
65
101
  return closed;
66
- },
102
+ }),
67
103
  update(next) {
68
- if (next.identity !== mount.identity)
104
+ if (disposed || next.identity !== mount.identity)
69
105
  return false;
70
106
  data = next.data;
71
- if (!fiber && typeof work === 'object' && 'update' in work)
107
+ if (started && work === undefined)
108
+ changedDuringSetup = true;
109
+ else if (!fiber && typeof work === 'object' && work && 'update' in work)
72
110
  work.update?.();
73
111
  return true;
74
112
  },
75
113
  dispose,
76
114
  };
77
115
  }
116
+ export function startMount(element, mount, report = reportError) {
117
+ const lifetime = prepareMount(element, mount, report);
118
+ lifetime.start();
119
+ return lifetime;
120
+ }
package/dist/owner.d.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  import { type Snapshot } from './snapshot.js';
2
- import { Effect } from 'effect';
2
+ import * as Effect from 'effect/Effect';
3
3
  import { type Program, type TaskPolicy, type CommandSlot } from './program.js';
4
4
  import { type ReportError } from './errors.js';
5
5
  import type { UiRuntime } from './runtime.js';
6
6
  export type { TaskPolicy } from './program.js';
7
+ interface OwnedResource {
8
+ dispose(): void;
9
+ close?(): Effect.Effect<void, unknown>;
10
+ }
7
11
  export interface DisposableOwner {
8
12
  readonly disposed: boolean;
9
- readonly own: <A extends {
10
- dispose(): void;
11
- }>(resource: A) => A;
13
+ readonly own: <A extends OwnedResource>(resource: A) => A;
12
14
  }
13
15
  export interface ModelOwner<Model extends object, R = never> extends DisposableOwner {
14
16
  readonly source: Program<Model, never>;
@@ -24,9 +26,9 @@ export interface ModelOwner<Model extends object, R = never> extends DisposableO
24
26
  readonly run: (slot: CommandSlot, effect: Effect.Effect<unknown, unknown, R>, policy: TaskPolicy) => void;
25
27
  readonly cancel: (slot: CommandSlot) => void;
26
28
  readonly isRunning: (slot: CommandSlot) => boolean;
27
- readonly awaitIdle: (slot?: CommandSlot) => Promise<void>;
29
+ readonly awaitIdle: (slot?: CommandSlot) => Effect.Effect<void>;
28
30
  /** Interrupt and join work before closing owned dependencies in reverse order. */
29
- readonly close: () => Promise<void>;
31
+ readonly close: () => Effect.Effect<void, AggregateError>;
30
32
  readonly dispose: () => void;
31
33
  }
32
34
  type Options = {
package/dist/owner.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { protectSnapshot } from './snapshot.js';
2
- import { Effect } from 'effect';
2
+ import * as Cause from 'effect/Cause';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Exit from 'effect/Exit';
3
5
  import { program, } from './program.js';
4
6
  import { patchModel } from './state.js';
5
7
  import { runAll, reportError } from './errors.js';
@@ -7,7 +9,7 @@ export function modelOwner(initial, options = {}) {
7
9
  let disposed = false;
8
10
  let staged;
9
11
  const cleanups = [];
10
- let closing;
12
+ let resourcesDisposed = false;
11
13
  const source = program({
12
14
  initial,
13
15
  ...options,
@@ -57,42 +59,27 @@ export function modelOwner(initial, options = {}) {
57
59
  return;
58
60
  disposed = true;
59
61
  source.dispose();
60
- runAll(cleanups
61
- .splice(0)
62
- .reverse()
63
- .map((resource) => () => resource.dispose()), options.onDefect ?? reportError);
62
+ resourcesDisposed = true;
63
+ runAll([...cleanups].reverse().map((resource) => () => resource.dispose()), options.onDefect ?? reportError);
64
64
  };
65
- const close = () => {
66
- if (closing)
67
- return closing;
65
+ const closing = Effect.runSync(Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
68
66
  disposed = true;
69
67
  const resources = cleanups.splice(0).reverse();
70
- let finish;
71
- let fail;
72
- closing = new Promise((resolve, reject) => {
73
- finish = resolve;
74
- fail = reject;
75
- });
76
- const closeResources = async () => {
77
- await source.close();
78
- const errors = [];
79
- for (const resource of resources) {
80
- try {
81
- if (resource.close)
82
- await resource.close();
83
- else
84
- resource.dispose();
85
- }
86
- catch (error) {
87
- errors.push(error);
88
- }
89
- }
90
- if (errors.length)
91
- throw new AggregateError(errors, 'Owner cleanup failed.');
92
- };
93
- closeResources().then(finish, fail);
94
- return closing;
95
- };
68
+ yield* source.close();
69
+ const errors = [];
70
+ for (const resource of resources) {
71
+ const exit = yield* Effect.exit(Effect.suspend(() => resource.close
72
+ ? resource.close()
73
+ : resourcesDisposed
74
+ ? Effect.void
75
+ : Effect.sync(() => resource.dispose())));
76
+ if (Exit.isFailure(exit))
77
+ errors.push(Cause.squash(exit.cause));
78
+ }
79
+ if (errors.length)
80
+ return yield* Effect.fail(new AggregateError(errors, 'Owner cleanup failed.'));
81
+ }))));
82
+ const close = () => closing;
96
83
  return {
97
84
  source: { model: source.model, send: source.send, subscribe: source.subscribe, dispose, close },
98
85
  read,
@@ -122,11 +109,6 @@ export function modelOwner(initial, options = {}) {
122
109
  staged = batch;
123
110
  try {
124
111
  const result = work();
125
- if (result &&
126
- (typeof result === 'object' || typeof result === 'function') &&
127
- 'then' in result &&
128
- typeof result.then === 'function')
129
- throw new TypeError('Model transactions must be synchronous. Run async work as an owned Effect.');
130
112
  staged = parent;
131
113
  if (!disposed) {
132
114
  if (parent) {
package/dist/pages.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
- import { Cause } from 'effect';
2
+ import * as Cause from 'effect/Cause';
3
3
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
4
4
  import type { UiLoad, UiPage } from './load.js';
5
5
  import { type Transition, type RunningProgram } from './program.js';
package/dist/pages.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { commandSlot } from './program.js';
2
2
  import { shareData } from './sharing.js';
3
- import { Cause, Option } from 'effect';
3
+ import * as Cause from 'effect/Cause';
4
+ import * as Option from 'effect/Option';
4
5
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
5
6
  import { effectCommand, program, } from './program.js';
6
7
  import { defaultUiRuntime } from './runtime.js';
package/dist/program.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type Snapshot } from './snapshot.js';
2
- import { Cause, Effect, Stream } from 'effect';
2
+ import * as Cause from 'effect/Cause';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Stream from 'effect/Stream';
3
5
  export type Send<Message> = (message: Message) => void;
4
6
  /** queue retains FIFO requests; latest-queued retains only the newest pending request. */
5
7
  export type TaskPolicy = 'replace' | 'drop' | 'parallel' | 'queue' | 'latest-queued';
@@ -51,14 +53,14 @@ export interface Program<Model, Message> {
51
53
  readonly send: Send<Message>;
52
54
  readonly subscribe: (listener: (model: Snapshot<Model>) => void) => () => void;
53
55
  readonly dispose: () => void;
54
- readonly close?: () => Promise<void>;
56
+ readonly close?: () => Effect.Effect<void, unknown>;
55
57
  }
56
58
  export interface RunningProgram<Model, Message> extends Program<Model, Message> {
57
59
  /** Wait for interrupted work and its finalizers as well as admitted work. */
58
- readonly awaitStopped: () => Promise<void>;
59
- readonly close: () => Promise<void>;
60
+ readonly awaitStopped: () => Effect.Effect<void>;
61
+ readonly close: () => Effect.Effect<void>;
60
62
  readonly activeSlots: () => readonly CommandSlot[];
61
- readonly awaitIdle: (slot?: CommandSlot) => Promise<void>;
63
+ readonly awaitIdle: (slot?: CommandSlot) => Effect.Effect<void>;
62
64
  }
63
65
  export declare function program<Model, Message>(options: {
64
66
  initial: Model | Snapshot<Model>;