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.
Files changed (66) hide show
  1. package/dist/AsyncContent.d.ts +1 -1
  2. package/dist/AsyncContent.js +18 -35
  3. package/dist/cache-internals.d.ts +3 -2
  4. package/dist/cache.d.ts +15 -12
  5. package/dist/cache.js +51 -10
  6. package/dist/collection.d.ts +1 -1
  7. package/dist/collection.js +1 -3
  8. package/dist/component.d.ts +1 -1
  9. package/dist/component.js +9 -4
  10. package/dist/dom.d.ts +64 -31
  11. package/dist/dom.js +237 -121
  12. package/dist/effectEvent.d.ts +10 -7
  13. package/dist/effectEvent.js +50 -46
  14. package/dist/form.d.ts +2 -1
  15. package/dist/http.d.ts +2 -1
  16. package/dist/http.js +1 -1
  17. package/dist/index.d.ts +10 -8
  18. package/dist/index.js +10 -8
  19. package/dist/infinite-query.d.ts +12 -8
  20. package/dist/infinite-query.js +21 -13
  21. package/dist/jsx-runtime.d.ts +6 -0
  22. package/dist/jsx-runtime.js +14 -1
  23. package/dist/jsx.d.ts +4 -1
  24. package/dist/keyed-tasks.d.ts +8 -6
  25. package/dist/keyed-tasks.js +29 -24
  26. package/dist/lazy.d.ts +4 -2
  27. package/dist/lazy.js +16 -10
  28. package/dist/load.d.ts +1 -1
  29. package/dist/load.js +1 -1
  30. package/dist/mount.d.ts +23 -8
  31. package/dist/mount.js +116 -27
  32. package/dist/owner.d.ts +13 -8
  33. package/dist/owner.js +37 -41
  34. package/dist/pages.d.ts +1 -1
  35. package/dist/pages.js +2 -1
  36. package/dist/program.d.ts +20 -8
  37. package/dist/program.js +44 -22
  38. package/dist/query-internals.d.ts +5 -3
  39. package/dist/query.d.ts +17 -6
  40. package/dist/query.js +6 -3
  41. package/dist/render.d.ts +17 -0
  42. package/dist/render.js +48 -0
  43. package/dist/resource.d.ts +4 -3
  44. package/dist/resource.js +12 -6
  45. package/dist/runtime.d.ts +12 -1
  46. package/dist/runtime.js +27 -5
  47. package/dist/session.d.ts +3 -2
  48. package/dist/session.js +15 -5
  49. package/dist/settlement.d.ts +8 -1
  50. package/dist/settlement.js +20 -8
  51. package/dist/snapshot.d.ts +1 -1
  52. package/dist/snapshot.js +10 -12
  53. package/dist/source.d.ts +38 -0
  54. package/dist/source.js +218 -0
  55. package/dist/task.d.ts +1 -1
  56. package/dist/task.js +3 -1
  57. package/dist/tasks.d.ts +9 -6
  58. package/dist/tasks.js +7 -4
  59. package/dist/testing.d.ts +5 -4
  60. package/dist/testing.js +4 -2
  61. package/package.json +10 -3
  62. package/README.md +0 -101
  63. package/dist/intrinsic.d.ts +0 -2
  64. package/dist/intrinsic.js +0 -120
  65. package/dist/slotIdentity.d.ts +0 -1
  66. package/dist/slotIdentity.js +0 -2
@@ -1,4 +1,4 @@
1
- import { Cause } from 'effect';
1
+ import * as Cause from 'effect/Cause';
2
2
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
3
  import { type Slot, type View } from './index.js';
4
4
  import type { JSX } from './jsx.js';
@@ -1,6 +1,8 @@
1
1
  import * as _ew_dom from "./dom.js";
2
2
  import { commandSlot } from "./program.js";
3
- import { Cause, Effect, Option } from "effect";
3
+ import * as Cause from "effect/Cause";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
4
6
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
7
  import { component } from "./component.js";
6
8
  import { view } from "./index.js";
@@ -47,40 +49,21 @@ const implementation = component({
47
49
  ...model,
48
50
  visible: true
49
51
  } }),
50
- view: /* @__PURE__ */ _ew_dom.compiled((_ew_scope_1, _ew_parent_2, _ew_before_3) => {
51
- const _ew_derived_4 = _ew_scope_1.derive(() => [_ew_scope_1.value?.props], () => {
52
- const _ew_capture_6 = _ew_scope_1.value;
53
- return _ew_dom.intrinsic(_ew_dom.intrinsic(_ew_capture_6, ["props", "content"], "@slot"), ["props", "failure"], "@slot").props;
54
- });
55
- const _ew_derived_7 = _ew_scope_1.derive(() => [_ew_derived_4()?.result], () => {
56
- const _ew_capture_9 = _ew_derived_4();
57
- return AsyncResult.value(_ew_capture_9.result);
58
- });
59
- const _ew_derived_10 = _ew_scope_1.derive(() => [_ew_derived_4()?.result, _ew_derived_4()?.result?.waiting], () => {
60
- const _ew_capture_12 = _ew_derived_4();
61
- return AsyncResult.isFailure(_ew_capture_12.result) && !_ew_capture_12.result.waiting ? _ew_capture_12.result : undefined;
62
- });
63
- _ew_dom.branch(_ew_scope_1, _ew_parent_2, _ew_before_3, () => Option.isSome(_ew_derived_7()), (_ew_scope_1, _ew_parent_13, _ew_before_14) => {
64
- _ew_dom.text(_ew_scope_1, _ew_parent_13, _ew_before_14, () => [_ew_derived_4(), _ew_derived_7()?.value], () => _ew_derived_4().content(_ew_derived_7().value));
65
- _ew_dom.branch(_ew_scope_1, _ew_parent_13, _ew_before_14, () => _ew_derived_4().result.waiting, (_ew_scope_1, _ew_parent_15, _ew_before_16) => {
66
- _ew_dom.text(_ew_scope_1, _ew_parent_15, _ew_before_16, () => [_ew_derived_4()?.refreshing], () => _ew_derived_4().refreshing);
67
- }, (_ew_scope_1, _ew_parent_15, _ew_before_16) => { });
68
- _ew_dom.branch(_ew_scope_1, _ew_parent_13, _ew_before_14, () => _ew_derived_10() && _ew_derived_4().failure, (_ew_scope_1, _ew_parent_17, _ew_before_18) => {
69
- _ew_dom.text(_ew_scope_1, _ew_parent_17, _ew_before_18, () => [_ew_derived_4(), _ew_derived_10()?.cause], () => _ew_derived_4().failure(_ew_derived_10().cause));
70
- }, (_ew_scope_1, _ew_parent_17, _ew_before_18) => { });
71
- }, (_ew_scope_1, _ew_parent_13, _ew_before_14) => {
72
- _ew_dom.branch(_ew_scope_1, _ew_parent_13, _ew_before_14, () => _ew_derived_10() && _ew_derived_4().failure, (_ew_scope_1, _ew_parent_19, _ew_before_20) => {
73
- _ew_dom.text(_ew_scope_1, _ew_parent_19, _ew_before_20, () => [_ew_derived_4(), _ew_derived_10()?.cause], () => _ew_derived_4().failure(_ew_derived_10().cause));
74
- }, (_ew_scope_1, _ew_parent_19, _ew_before_20) => {
75
- _ew_dom.branch(_ew_scope_1, _ew_parent_19, _ew_before_20, () => _ew_scope_1.value.pending, (_ew_scope_1, _ew_parent_21, _ew_before_22) => {
76
- _ew_dom.branch(_ew_scope_1, _ew_parent_21, _ew_before_22, () => _ew_scope_1.value.visible, (_ew_scope_1, _ew_parent_23, _ew_before_24) => {
77
- _ew_dom.text(_ew_scope_1, _ew_parent_23, _ew_before_24, () => [_ew_derived_4()?.pending], () => _ew_derived_4().pending);
78
- }, (_ew_scope_1, _ew_parent_23, _ew_before_24) => { });
79
- }, (_ew_scope_1, _ew_parent_21, _ew_before_22) => {
80
- _ew_dom.text(_ew_scope_1, _ew_parent_21, _ew_before_22, () => [_ew_derived_4()?.empty], () => _ew_derived_4().empty);
81
- });
82
- });
83
- });
52
+ view: view((model, _send) => {
53
+ const props = model.props;
54
+ const data = AsyncResult.value(props.result);
55
+ const failure = AsyncResult.isFailure(props.result) && !props.result.waiting ? props.result : undefined;
56
+ if (Option.isSome(data))
57
+ return [
58
+ props.content(data.value),
59
+ props.result.waiting ? props.refreshing : null,
60
+ failure && props.failure ? props.failure(failure.cause) : null
61
+ ];
62
+ if (failure && props.failure)
63
+ return props.failure(failure.cause);
64
+ if (model.pending)
65
+ return model.visible ? props.pending : null;
66
+ return props.empty;
84
67
  })
85
68
  });
86
69
  /** Presentation only: resource owners choose identity, loading, caching and cancellation. */
@@ -1,3 +1,4 @@
1
+ import type * as Scope from 'effect/Scope';
1
2
  import type * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
2
3
  import type * as Atom from 'effect/unstable/reactivity/Atom';
3
4
  import type * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
@@ -7,11 +8,11 @@ import type { Snapshot } from './snapshot.js';
7
8
  export interface CacheInternals<R> {
8
9
  refresh(atom: Atom.Atom<unknown>): void;
9
10
  readonly disposed: () => boolean;
10
- revision<Args, A, E>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): number | undefined;
11
+ revision<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: Args | Snapshot<Args>): number | undefined;
11
12
  retain(atom: Atom.Atom<unknown>): () => void;
12
13
  readonly registry: AtomRegistry.AtomRegistry;
13
14
  readonly generation: Atom.Writable<number>;
14
- query<Args, A, E>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): Atom.Atom<AsyncResult.AsyncResult<Snapshot<A>, E>>;
15
+ query<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: Args | Snapshot<Args>): Atom.Atom<AsyncResult.AsyncResult<Snapshot<A>, E>>;
15
16
  }
16
17
  export declare function registerCache<R>(cache: QueryCache<R>, internals: CacheInternals<R>): void;
17
18
  /** Private implementation access; deliberately absent from package exports. */
package/dist/cache.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { type Snapshot } from './snapshot.js';
2
- import { Effect } from 'effect';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Scope from 'effect/Scope';
3
4
  import { type Query, type QueryGroup } from './query.js';
4
- import type { UiRuntime } from './runtime.js';
5
+ import { type UiRuntime } from './runtime.js';
5
6
  export { loadEffect, type UiLoad } from './load.js';
6
7
  export { shareValue } from './share.js';
7
8
  export interface QueryCacheOptions {
@@ -10,32 +11,34 @@ export interface QueryCacheOptions {
10
11
  }
11
12
  export declare function makeQueryCache(options?: QueryCacheOptions): QueryCache<never>;
12
13
  export declare function makeQueryCache<R>(runtime: UiRuntime<R>, options?: QueryCacheOptions): QueryCache<R>;
14
+ /** Capture application services and clock, and close the cache with the current Effect scope. */
15
+ export declare const scopedQueryCache: <R = never>(options?: QueryCacheOptions) => Effect.Effect<QueryCache<R>, never, R | Scope.Scope>;
13
16
  /** A cache owns one registry and the query resources published through it. */
14
17
  export interface QueryCache<R = never> {
15
18
  batch(work: () => void): void;
16
- getQueryData<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>): Snapshot<A> | undefined;
17
- invalidateWhere<Args, A, E>(definition: Query<Args, A, E, R>, predicate: (args: Snapshot<Args>) => boolean): void;
19
+ getQueryData<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>): Snapshot<A> | undefined;
20
+ invalidateWhere<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, predicate: (args: Snapshot<Args>) => boolean): void;
18
21
  invalidateGroup(group: QueryGroup): void;
19
22
  /** Cancel requests while retaining the last success. Explicit refresh restarts them. */
20
- cancelQuery<Args, A, E>(definition: Query<Args, A, E, R>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
23
+ cancelQuery<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
21
24
  /** Clear cached data and cancel requests; a subsequent selection or refresh reloads. */
22
- removeQuery<Args, A, E>(definition: Query<Args, A, E, R>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
23
- prefetch<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, options?: {
25
+ removeQuery<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
26
+ prefetch<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, options?: {
24
27
  readonly refresh?: boolean;
25
28
  }): Effect.Effect<Snapshot<A>, E>;
26
29
  /** Publish a protected success, including undefined, and supersede any pending load for this key. */
27
- setQueryData<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, value: NoInfer<A> | Snapshot<NoInfer<A>>): Snapshot<A>;
30
+ setQueryData<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, value: NoInfer<A> | Snapshot<NoInfer<A>>): Snapshot<A>;
28
31
  /**
29
32
  * Update an existing success, including one retained during refresh or failure, and supersede its load.
30
33
  * No success or an undefined updater return skips the write. Use setQueryData to seed or store undefined.
31
34
  * Updaters run synchronously on readonly data; a throw leaves the cached value unchanged.
32
35
  */
33
- updateQueryData<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, update: (previous: Snapshot<A>) => NoInfer<A> | Snapshot<NoInfer<A>> | undefined): Snapshot<A> | undefined;
34
- invalidateQuery<Args, A, E>(definition: Query<Args, A, E, R>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
36
+ updateQueryData<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, update: (previous: Snapshot<A>) => NoInfer<A> | Snapshot<NoInfer<A>> | undefined): Snapshot<A> | undefined;
37
+ invalidateQuery<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
35
38
  /** Observe account resets without exposing registry mutation. */
36
39
  onReset(listener: () => void): () => void;
37
40
  resetResources(): void;
38
- /** Interrupt every request and wait for its finalizers, including previously canceled requests. */
39
- close(): Promise<void>;
41
+ /** Release retained acquisitions and join all load/resource finalizers, including canceled or evicted entries. */
42
+ close(): Effect.Effect<void>;
40
43
  dispose(): void;
41
44
  }
package/dist/cache.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import { Settlement } from './settlement.js';
2
2
  import { protectSnapshot } from './snapshot.js';
3
- import { Deferred, Effect, Option } from 'effect';
3
+ import * as Deferred from 'effect/Deferred';
4
+ import * as Effect from 'effect/Effect';
5
+ import * as Scope from 'effect/Scope';
6
+ import * as Exit from 'effect/Exit';
7
+ import * as Option from 'effect/Option';
4
8
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
5
9
  import * as Atom from 'effect/unstable/reactivity/Atom';
6
10
  import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
7
11
  import { loadEffect } from './load.js';
8
12
  import { shareData } from './sharing.js';
9
- import { encodeQueryKey } from './query.js';
13
+ import { encodeQueryArguments } from './query.js';
10
14
  import { registerCache } from './cache-internals.js';
11
15
  import { queryDefinition } from './query-internals.js';
16
+ import { defaultUiRuntime, makeUiRuntime } from './runtime.js';
17
+ import { reportError, reportSafely } from './errors.js';
12
18
  export { loadEffect } from './load.js';
13
19
  export { shareValue } from './share.js';
14
20
  export function makeQueryCache(runtimeOrOptions, options = {}) {
@@ -21,6 +27,7 @@ function createQueryCache(runtime, options = {}) {
21
27
  if (!Number.isFinite(retention) || retention < 0)
22
28
  throw new RangeError('Query retention must be finite and nonnegative.');
23
29
  const registry = AtomRegistry.make({ defaultIdleTTL: retention });
30
+ const clock = (runtime ?? defaultUiRuntime).clock;
24
31
  const cancelValue = Symbol('cancel');
25
32
  const removeValue = Symbol('remove');
26
33
  const generation = Atom.keepAlive(Atom.make(0));
@@ -52,8 +59,28 @@ function createQueryCache(runtime, options = {}) {
52
59
  next.revision = ++nextRevision;
53
60
  next.canceled = false;
54
61
  const revision = next.revision;
62
+ const scope = Scope.makeUnsafe();
63
+ const completed = Deferred.makeUnsafe();
64
+ const finish = settlement.begin();
65
+ let started = false;
66
+ let stopping = false;
67
+ get.addFinalizer(() => {
68
+ stopping = true;
69
+ // The atom interrupts its load first. Join acquisition and its ensuring
70
+ // finalizers before closing resources, including late acquisitions.
71
+ Effect.runFork(Effect.uninterruptible(Effect.gen(function* () {
72
+ if (started)
73
+ yield* Deferred.await(completed);
74
+ yield* Scope.close(scope, Exit.void);
75
+ })).pipe(Effect.ensuring(Effect.sync(finish)))).addObserver((exit) => {
76
+ if (Exit.isFailure(exit))
77
+ reportSafely(reportError, exit.cause);
78
+ });
79
+ });
55
80
  return Effect.suspend(() => {
56
- const finish = settlement.begin();
81
+ if (stopping)
82
+ return Effect.interrupt;
83
+ started = true;
57
84
  return loadEffect(() => next.load()).pipe(Effect.map((value) => {
58
85
  const shared = Option.isSome(previous)
59
86
  ? share(previous.value, value)
@@ -62,7 +89,12 @@ function createQueryCache(runtime, options = {}) {
62
89
  if (!disposed && revision === next.revision)
63
90
  remember(next, snapshot);
64
91
  return snapshot;
65
- }), Effect.ensuring(Effect.sync(finish)));
92
+ }), Scope.provide(scope), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void).pipe(Effect.ensuring(Effect.sync(() => {
93
+ Deferred.doneUnsafe(completed, Effect.void);
94
+ // Successful loads retain their resources with the atom entry.
95
+ if (Exit.isFailure(exit))
96
+ finish();
97
+ })))));
66
98
  });
67
99
  });
68
100
  const next = {
@@ -110,7 +142,7 @@ function createQueryCache(runtime, options = {}) {
110
142
  return { entry, atom: entry.atom };
111
143
  };
112
144
  const remember = (entry, value) => {
113
- entry.loadedAt = Date.now();
145
+ entry.loadedAt = clock.currentTimeMillisUnsafe();
114
146
  const node = registry.getNodes().get(entry.atom);
115
147
  if (node)
116
148
  values.set(node, { value });
@@ -123,13 +155,15 @@ function createQueryCache(runtime, options = {}) {
123
155
  if (disposed)
124
156
  throw new Error('Cannot write to a disposed query cache.');
125
157
  };
126
- const queryKey = (definition, args) => `query:${identity(definition)}:${encodeQueryKey(args)}`;
158
+ const queryKey = (definition, args) => `query:${identity(definition)}:${encodeQueryArguments(definition, args)}`;
127
159
  const acquireQuery = (definition, args) => {
128
160
  const config = queryDefinition(definition);
129
161
  protectSnapshot(args);
130
162
  const { entry, atom } = acquire(queryKey(definition, args), () => {
131
163
  const effect = Effect.suspend(() => config.load(args, previousValue(resources.get(queryKey(definition, args)))?.value));
132
- return runtime ? runtime.provide(effect) : effect;
164
+ return runtime
165
+ ? runtime.provideScoped(effect)
166
+ : effect;
133
167
  }, config.share);
134
168
  entry.query = definition;
135
169
  entry.args = args;
@@ -147,7 +181,7 @@ function createQueryCache(runtime, options = {}) {
147
181
  refresh(entry);
148
182
  if (registry.getNodes().has(atom) &&
149
183
  entry.loadedAt !== undefined &&
150
- Date.now() - entry.loadedAt >= config.staleTime) {
184
+ clock.currentTimeMillisUnsafe() - entry.loadedAt >= config.staleTime) {
151
185
  const current = registry.get(atom);
152
186
  if (!current.waiting)
153
187
  refresh(entry);
@@ -292,8 +326,10 @@ function createQueryCache(runtime, options = {}) {
292
326
  },
293
327
  close() {
294
328
  if (!closing) {
295
- cache.dispose();
296
- closing = settlement.wait();
329
+ closing = Effect.suspend(() => {
330
+ cache.dispose();
331
+ return settlement.wait();
332
+ });
297
333
  }
298
334
  return closing;
299
335
  },
@@ -322,3 +358,8 @@ function createQueryCache(runtime, options = {}) {
322
358
  });
323
359
  return Object.freeze(cache);
324
360
  }
361
+ /** Capture application services and clock, and close the cache with the current Effect scope. */
362
+ export const scopedQueryCache = (options = {}) => Effect.gen(function* () {
363
+ const runtime = yield* makeUiRuntime();
364
+ return yield* Effect.acquireRelease(Effect.sync(() => makeQueryCache(runtime, options)), (cache) => cache.close());
365
+ });
@@ -9,7 +9,7 @@ export interface Rows<A> {
9
9
  slice(start?: number, end?: number): Rows<A>;
10
10
  }
11
11
  /** Validate without retaining values. Positions are zero-based array indices. */
12
- export declare function validateIdentities<A>(items: readonly A[], identity: (item: A, index: number) => Identity): Identity[];
12
+ export declare function validateIdentities<A, K>(items: readonly A[], identity: (item: A, index: number) => K): K[];
13
13
  export interface Collection<A> {
14
14
  from(this: void, items: readonly (A | Snapshot<A>)[]): Rows<Snapshot<A>>;
15
15
  share<B extends A | Snapshot<A>>(this: void, previous: readonly B[], next: readonly B[]): readonly Snapshot<B>[];
@@ -2,10 +2,8 @@ import { shareData } from './sharing.js';
2
2
  /** Validate without retaining values. Positions are zero-based array indices. */
3
3
  export function validateIdentities(items, identity) {
4
4
  const seen = new Map();
5
- return items.map((item, index) => {
5
+ return Array.from(items, (item, index) => {
6
6
  const key = identity(item, index);
7
- if (typeof key !== 'string' && typeof key !== 'number')
8
- throw new Error(`Invalid collection identity at index ${index}. Use a string or number domain identity, or sequence(items) for positional identity.`);
9
7
  const previous = seen.get(key);
10
8
  if (previous !== undefined)
11
9
  throw new Error(`Duplicate collection identity at indices ${previous} and ${index}. Identity must be unique within the collection; use a composite domain identity when IDs are only locally unique.`);
@@ -31,7 +31,7 @@ export declare function component<Props, Model extends {
31
31
  /** Mount an existing program without introducing a second state owner. */
32
32
  export declare function programView<Props, Model, Message>(definition: {
33
33
  identity?: (props: Snapshot<Props>) => unknown;
34
- create: (props: Snapshot<Props>) => import('./program').Program<Model, Message>;
34
+ create: (props: Snapshot<Props>, runtime: UiRuntime<never>) => import('./program').Program<Model, Message>;
35
35
  receive: (source: import('./program').Program<Model, Message>, props: Snapshot<Props>) => void;
36
36
  view: View<Model, Message>;
37
37
  }): View<Props, never>;
package/dist/component.js CHANGED
@@ -1,3 +1,5 @@
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Effect from 'effect/Effect';
1
3
  import { compiled, Scope, viewRegion } from './dom.js';
2
4
  import { mapCommand, program } from './program.js';
3
5
  import { patchModel } from './state.js';
@@ -17,8 +19,9 @@ export function localComponent(definition) {
17
19
  }
18
20
  /** A child model receives immutable parent inputs and owns its own message/command loop. */
19
21
  export function component(definition) {
20
- const runtime = definition.runtime ?? defaultUiRuntime;
21
22
  return identify(compiled((scope, parent, before) => {
23
+ const ownerRuntime = scope.settlement.runtime ?? defaultUiRuntime;
24
+ const runtime = definition.runtime ?? ownerRuntime;
22
25
  const wrap = (next) => ({
23
26
  model: next.model,
24
27
  ...(next.cancel ? { cancel: next.cancel } : {}),
@@ -31,6 +34,7 @@ export function component(definition) {
31
34
  const source = program({
32
35
  initial: definition.init(scope.value),
33
36
  onDefect: scope.report,
37
+ runtime: ownerRuntime,
34
38
  update: (model, envelope) => wrap(envelope.type === 'Input'
35
39
  ? (definition.receive?.(model, envelope.props) ?? {
36
40
  model: Object.is(model.props, envelope.props)
@@ -60,7 +64,7 @@ export function component(definition) {
60
64
  /** Mount an existing program without introducing a second state owner. */
61
65
  export function programView(definition) {
62
66
  return identify(compiled((scope, parent, before) => {
63
- const source = definition.create(scope.value);
67
+ const source = definition.create(scope.value, scope.settlement.runtime ?? defaultUiRuntime);
64
68
  if (scope.disposed) {
65
69
  closeProgram(scope, source);
66
70
  return;
@@ -82,9 +86,10 @@ function closeProgram(scope, source) {
82
86
  const finish = scope.settlement.begin();
83
87
  try {
84
88
  if (source.close) {
85
- source.close().then(finish, (error) => {
89
+ Effect.runFork(source.close()).addObserver((exit) => {
86
90
  finish();
87
- reportSafely(scope.report, error);
91
+ if (exit._tag === 'Failure')
92
+ reportSafely(scope.report, Cause.squash(exit.cause));
88
93
  });
89
94
  }
90
95
  else {
package/dist/dom.d.ts CHANGED
@@ -1,12 +1,16 @@
1
- import type { Snapshot } from './snapshot.js';
1
+ import * as Effect from 'effect/Effect';
2
+ import { type Snapshot } from './snapshot.js';
2
3
  import { type ReportError } from './errors.js';
3
4
  import { type BindingSource } from './diagnostics.js';
4
5
  import type { Rows } from './index.js';
5
6
  import { jsxComponent, type JSX } from './jsx.js';
6
7
  import type { DomMount } from './mount.js';
7
8
  import { Settlement } from './settlement.js';
8
- import type { Program, Send } from './program.js';
9
+ import type { Send } from './program.js';
10
+ import type { Source } from './source.js';
11
+ import type { UiRuntime } from './runtime.js';
9
12
  type Dependencies = () => readonly unknown[];
13
+ type BindingLocation = BindingSource | (() => BindingSource | undefined);
10
14
  type Cleanup = () => void;
11
15
  type Build<M, E> = (scope: Scope<M, E>, parent: Node, before: Node | null) => void;
12
16
  /** Compiler implementation. No implicit tracking, proxies, or per-binding subscriptions. */
@@ -18,10 +22,8 @@ export declare class Scope<M, E> {
18
22
  readonly jobs: Cleanup[];
19
23
  readonly cleanups: Cleanup[];
20
24
  disposed: boolean;
21
- private revision;
22
25
  constructor(value: M, send: Send<E>, report?: ReportError, settlement?: Settlement);
23
- derive<A>(dependencies: Dependencies, compute: () => A, source?: BindingSource): () => A;
24
- watch(dependencies: Dependencies, apply: () => void, source?: BindingSource): void;
26
+ watch(dependencies: Dependencies, apply: () => void, source?: BindingLocation): void;
25
27
  set(value: M): void;
26
28
  dispose(): void;
27
29
  }
@@ -29,8 +31,8 @@ export interface View<M, E> extends JSX.ComponentType {
29
31
  (this: never, props: [E] extends [never] ? M | Snapshot<M> : never): JSX.Element;
30
32
  readonly build: Build<M, E>;
31
33
  }
32
- /** Compiler marker for mounting a view with an explicit model and message dispatcher. */
33
- export declare const ViewBinding: (<M, E>(this: never, _props: {
34
+ /** Mount a view with an explicit model and message dispatcher. */
35
+ export declare const ViewBinding: (<M, E>(this: never, props: {
34
36
  view: View<M, E>;
35
37
  model: NoInfer<M> | Snapshot<NoInfer<M>>;
36
38
  send: Send<NoInfer<E>>;
@@ -38,51 +40,82 @@ export declare const ViewBinding: (<M, E>(this: never, _props: {
38
40
  [jsxComponent]: true;
39
41
  };
40
42
  /** Untyped callers get an actionable failure instead of silently losing child messages. */
41
- export declare function unboundSend(_message: never): never;
43
+ export declare function unboundSend(_message: unknown): never;
42
44
  declare const contentBrand: unique symbol;
43
45
  /** Owned markup accepted wherever JSX can render content. */
44
46
  export interface CompiledContent {
45
47
  readonly [contentBrand]: true;
46
48
  }
47
- /** A compiled template with a typed placement value, captured in its declaring view. */
49
+ /** An ordinary render callback with a typed input. Invoke it to produce content. */
48
50
  export interface Slot<A = void> {
49
51
  (value: A | Snapshot<A>): JSX.Element;
50
- readonly [contentBrand]: [A] extends [void] ? true : false;
51
52
  }
52
- /** Compiler marker: declare inside view(), or directly in a compiled component prop. */
53
- export declare function slot<A = void>(_render: (value: Snapshot<A>) => JSX.Element): Slot<A>;
54
- /** Compiler output. Captures follow their declaring scope; placements own independent scopes. */
55
- export declare function compiledSlot<M, E, A>(owner: Scope<M, E>, build: Build<A, E>): Slot<A>;
56
- /** This declaration is a compiler marker, never a component setup callback. */
57
- export declare function view<M, E = never>(_render: (model: Snapshot<M>, send: Send<E>) => JSX.Element): View<M, E>;
53
+ export declare function slot<A = void>(render: (value: Snapshot<A>) => JSX.Element): Slot<A>;
54
+ /** Evaluate ordinary synchronous JavaScript for each immutable model publication. */
55
+ export declare function view<M, E = never>(render: (model: Snapshot<M>, send: Send<E>) => JSX.Element): View<M, E>;
58
56
  export declare function compiled<M, E>(build: Build<M, E>): View<M, E>;
57
+ /** Skip a view only under the caller's explicit equality contract. */
58
+ export declare function memoView<M, E>(definition: View<M, E>, equals: (previous: Snapshot<M>, next: Snapshot<M>) => boolean): View<M, E>;
59
+ type MarkupProps = Readonly<Record<string, unknown>> & {
60
+ readonly children?: JSX.Element;
61
+ };
62
+ /** JSX factories preserve host identity by element type; source locations are metadata only. */
63
+ export declare function markup(tag: string, sources?: Readonly<Record<string, BindingSource>>): (props: MarkupProps) => JSX.Element;
64
+ export declare function renderComponent(component: (props: never) => JSX.Element, props: Readonly<Record<string, unknown>>): JSX.Element;
65
+ /** Preserve collection identity (or array reference identity) while rendering rows. */
66
+ export declare function list<A>(rows: Rows<A> | readonly A[], render: (item: A, index: number) => JSX.Element): JSX.Element;
67
+ /**
68
+ * Define keyed row projection once. Every dependency is passed as an explicit input;
69
+ * only changed projected props enter the row view's renderer.
70
+ */
71
+ export declare function listView<A, Input, Props, Message = never>(options: {
72
+ readonly project: (item: A, index: number, input: Input) => Props | Snapshot<Props>;
73
+ readonly view: View<Props, Message>;
74
+ readonly equals?: (previous: Snapshot<Props>, next: Snapshot<Props>) => boolean;
75
+ }): (rows: Rows<A> | readonly A[], input: Input, ...dispatch: [Message] extends [never] ? [send?: Send<Message>] : [send: Send<Message>]) => JSX.Element;
76
+ /** Render a declared source in its own subscribed region. No dependencies are inferred. */
77
+ export declare function observe<A>(source: Source<A>, render: (value: Snapshot<A>) => JSX.Element): JSX.Element;
59
78
  export interface PortalProps {
60
79
  readonly children?: JSX.Element;
61
80
  readonly mount?: Element | undefined;
62
81
  }
63
82
  /** Owned content in an element target, defaulting to the document body. */
64
83
  export declare const Portal: View<PortalProps, never>;
65
- export declare function mountView<M, E>(parent: Node, definition: View<M, E>, source: Program<M, E>, options?: {
66
- onError?: ReportError;
67
- }): (() => void) & {
68
- dispose: () => void;
69
- close: () => Promise<void>;
70
- };
84
+ export interface MountOptions {
85
+ readonly onError?: ReportError;
86
+ readonly runtime?: UiRuntime<never>;
87
+ }
88
+ export interface Mounted {
89
+ (): void;
90
+ readonly dispose: () => void;
91
+ readonly close: () => Effect.Effect<void>;
92
+ }
93
+ export declare function mountView<M, E>(parent: Node, definition: View<M, E>, source: Source<M> & {
94
+ readonly send: Send<E>;
95
+ }, options?: MountOptions): Mounted;
96
+ export declare function mountView<M>(parent: Node, definition: View<M, never>, source: Source<M>, options?: MountOptions): Mounted;
97
+ export declare function mountView<M, E>(parent: Node, definition: View<M, E>, source: Source<M>, options: MountOptions & {
98
+ readonly send: Send<E>;
99
+ }): Mounted;
100
+ /** Internal mounting boundary. Its owner retains completion accounting if construction fails. */
101
+ export declare function mountViewWithSettlement<M, E>(parent: Node, definition: View<M, E>, source: Source<M>, options: {
102
+ readonly send: Send<E>;
103
+ readonly onError?: ReportError;
104
+ }, settlement: Settlement): Mounted;
71
105
  export declare function element(parent: Node, before: Node | null, tag: string): Element;
72
106
  export declare function attribute(element: Element, name: string, value: unknown): void;
73
107
  /** Compiler output for attributes whose unchanged value needs no DOM refresh. */
74
- export declare function bindAttribute<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
108
+ export declare function bindAttribute<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown, source?: BindingLocation): void;
75
109
  /** Handled edits can change a control even when dispatch publishes no new model. */
76
- export declare function bindControl<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
77
- export declare function text<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
78
- export declare function event<M, E>(scope: Scope<M, E>, element: Element, name: string, handler: (event: Event) => unknown): void;
79
- /** One handler identity owns its requests, regardless of the JSX attribute spelling. */
80
- export declare function bindEvent<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown): void;
110
+ export declare function bindControl<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown, source?: BindingLocation): void;
111
+ export declare function text<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => JSX.Element, source?: BindingLocation): void;
112
+ export declare function event<M, E>(scope: Scope<M, E>, element: Element, name: string, handler: (event: Event) => JSX.EventResult): void;
113
+ /** The event binding owns its requests; snapshot renders may replace its callback. */
114
+ export declare function bindEvent<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => ((event: Event) => JSX.EventResult) | undefined): void;
81
115
  /** Reconcile merged JSX attributes. Each host, event and control owns its cleanup. */
82
- export declare function bindAttributes<M, E>(scope: Scope<M, E>, element: Element, dependencies: Dependencies, read: () => Readonly<Record<string, unknown>>): void;
116
+ export declare function bindAttributes<M, E>(scope: Scope<M, E>, element: Element, dependencies: Dependencies, read: () => Readonly<Record<string, unknown>>, sources?: () => Readonly<Record<string, BindingSource>> | undefined): void;
83
117
  export declare function branch<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, choose: () => boolean, yes: Build<M, E>, no: Build<M, E>): void;
84
118
  export declare function each<M, E, A>(scope: Scope<M, E>, parent: Node, before: Node | null, read: () => Rows<A> | readonly A[], outer: Dependencies, indexUsed: boolean, build: Build<readonly [A, number], E>): void;
85
- export declare function invoke<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => readonly unknown[], build: Build<readonly unknown[], E>): void;
86
119
  export declare function child<M, E, C, F>(scope: Scope<M, E>, parent: Node, before: Node | null, definition: View<C, F>, dependencies: Dependencies, model: () => C, send: Send<F>): void;
87
120
  /** A late-bound compiled definition owns one replaceable DOM region. */
88
121
  export declare function viewRegion<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, options?: {
@@ -94,4 +127,4 @@ export declare function portal<M, E>(scope: Scope<M, E>, build: Build<M, E>, mou
94
127
  /** Build static markup once per document/namespace, then clone native nodes at each use. */
95
128
  export declare function template(build: string | ((parent: Node, before: Node | null) => void), depth?: number): (parent: Node, before: Node | null) => Node;
96
129
  export declare function literal(parent: Node, before: Node | null, value: string): void;
97
- export { intrinsic } from './intrinsic.js';
130
+ export {};