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,65 +1,69 @@
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
- import { defaultUiRuntime } from './runtime.js';
5
+ import { uiRuntime } from './runtime.js';
4
6
  const tag = Symbol('Effect event');
5
7
  export function effectEvent(policy, load, runtime) {
6
- const owner = runtime ?? defaultUiRuntime;
7
- return (event) => ({ [tag]: true, policy, effect: owner.provide(load(event)) });
8
+ return (event) => ({
9
+ [tag]: true,
10
+ policy,
11
+ effect: runtime
12
+ ? runtime.provideScoped(load(event))
13
+ : load(event),
14
+ });
8
15
  }
9
- /** Internal listener owner, allocated only when an event returns a value needing inspection. */
16
+ /** Capture an operation's services in Effect setup; its event listener supplies the work scope. */
17
+ export const makeEffectHandler = (work) => Effect.map(Effect.context(), (context) => {
18
+ const runtime = uiRuntime(context);
19
+ return (...args) => runtime.provideScoped(Effect.suspend(() => work(...args)));
20
+ });
21
+ /** Internal listener owner, allocated only when an event returns an owned request. */
10
22
  export function eventEffects(report, settlement) {
11
- let active;
23
+ const active = new Set();
12
24
  let disposed = false;
13
25
  const stop = () => {
14
- const previous = active;
15
- active = undefined;
16
- if (previous?.fiber)
17
- Effect.runFork(Fiber.interrupt(previous.fiber));
26
+ const previous = [...active];
27
+ active.clear();
28
+ for (const token of previous)
29
+ if (token.fiber)
30
+ Effect.runFork(Fiber.interrupt(token.fiber));
18
31
  };
19
32
  return {
20
33
  accept(value) {
34
+ // Direct Effects admit every event. Other policies are explicit requests.
35
+ const request = Effect.isEffect(value)
36
+ ? { policy: 'parallel', effect: value }
37
+ : value;
21
38
  if (disposed)
22
39
  return;
23
- if (value && typeof value === 'object' && tag in value) {
24
- const request = value;
25
- if (request.policy === 'drop' && active)
26
- return;
40
+ if (request.policy === 'drop' && active.size)
41
+ return;
42
+ if (request.policy === 'replace')
27
43
  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;
42
- active = undefined;
43
- if (exit._tag === 'Failure')
44
- reportSafely(report, exit.cause);
45
- });
46
- }
47
- catch (error) {
44
+ const token = {};
45
+ active.add(token);
46
+ const finished = settlement?.begin();
47
+ try {
48
+ const fiber = settlement?.runtime
49
+ ? settlement.runtime.runFork(request.effect)
50
+ : Effect.runFork(Effect.scoped(request.effect));
51
+ token.fiber = fiber;
52
+ fiber.addObserver((exit) => {
53
+ const current = !disposed && active.has(token);
54
+ active.delete(token);
55
+ if (exit._tag === 'Failure' && (current || !Cause.hasInterruptsOnly(exit.cause)))
56
+ reportSafely(report, exit.cause);
48
57
  finished?.();
49
- if (active === token)
50
- active = undefined;
51
- reportSafely(report, error);
58
+ });
59
+ if (disposed || !active.has(token)) {
60
+ Effect.runFork(Fiber.interrupt(fiber));
52
61
  }
53
62
  }
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.'));
63
+ catch (error) {
64
+ finished?.();
65
+ active.delete(token);
66
+ reportSafely(report, error);
63
67
  }
64
68
  },
65
69
  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. */
package/dist/index.d.ts CHANGED
@@ -1,17 +1,19 @@
1
1
  export type { Snapshot, SnapshotOpaque, snapshotOpaque } from './snapshot.js';
2
- export { modelOwner, type ModelOwner, type DisposableOwner, type TaskPolicy } from './owner.js';
2
+ export { modelOwner, makeModelOwner, type ModelOwner, type DisposableOwner, type TaskPolicy, } from './owner.js';
3
3
  export { collection, entities, sequence, type Rows, type Collection } from './collection.js';
4
4
  export { component, localComponent, programView } from './component.js';
5
5
  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
- export { effectEvent } from './effectEvent.js';
9
- export { mountView, view, ViewBinding, type View } from './dom.js';
8
+ export { effectEvent, makeEffectHandler } from './effectEvent.js';
9
+ export { mountView, view, list, listView, memoView, observe, ViewBinding, type View, type Mounted, } from './dom.js';
10
+ export { mount } from './render.js';
11
+ export { mapSource, fromStream, fromSubscriptionRef, fromAtom, projectionSource, type Source, } from './source.js';
10
12
  export { lazyView, type LazyViewOptions } from './lazy.js';
11
13
  export type { JSX } from './jsx.js';
12
- export { Portal, domBinding, domMount, type DomMount, type PortalProps } from './mount.js';
14
+ export { Portal, domBinding, domMount, makeDomMount, makeDomBinding, type DomMount, type PortalProps, } from './mount.js';
13
15
  export { pages, type PagesMessage, type PagesRequest, type PagesModel, type PagesProgram, type Pagination, } from './pages.js';
14
- export { program, mapTransition, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
16
+ export { program, makeProgram, mapTransition, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
15
17
  export { available, resourceComponent, resourceError, type ResourceMessage, type ResourceModel, } from './resource.js';
16
18
  export { fromPromise, type UiLoad, type UiPage } from './load.js';
17
19
  export { observeBindings, inspectBindings, mountBindingInspector, type BindingInspector, type BindingInspection, observePrograms, type BindingUpdate, type ProgramUpdate, } from './diagnostics.js';
@@ -19,9 +21,9 @@ export { inputText, inputChecked, inputNumber, submit, defineField, type FieldRe
19
21
  export { slot, type Slot, type CompiledContent } from './dom.js';
20
22
  export { AsyncContent, type AsyncContentProps } from './AsyncContent.js';
21
23
  export { defineTasks, type TaskDefinition, type TasksModel, type TasksMessage, type TaskResults, } from './tasks.js';
22
- export { uiRuntime, type UiRuntime } from './runtime.js';
23
- export { query, queryGroup, type Query, type QueryKey, type QueryGroup } from './query.js';
24
- export { makeQueryCache, type QueryCache, type QueryCacheOptions } from './cache.js';
24
+ export { uiRuntime, makeUiRuntime, type UiRuntime } from './runtime.js';
25
+ export { query, queryGroup, type Query, type QueryKey, type QueryGroup, type QueryEncoding, } from './query.js';
26
+ export { makeQueryCache, scopedQueryCache, type QueryCache, type QueryCacheOptions, } from './cache.js';
25
27
  export { queryResource, type QueryResource, observeQuery, lifetime, projectionCache, sessionGroup, type SessionContext, type Read, } from './session.js';
26
28
  export { httpJson } from './http.js';
27
29
  export { infiniteQuery, infiniteResource, type InfiniteData, type InfiniteQuery, type InfiniteResource, } from './infinite-query.js';
package/dist/index.js CHANGED
@@ -1,15 +1,17 @@
1
- export { modelOwner } from './owner.js';
1
+ export { modelOwner, makeModelOwner, } from './owner.js';
2
2
  export { collection, entities, sequence } from './collection.js';
3
3
  export { component, localComponent, programView } from './component.js';
4
4
  export { patchModel } from './state.js';
5
5
  export { defineActions } from './actions.js';
6
6
  export { taskComponent, taskControls } from './task.js';
7
- export { effectEvent } from './effectEvent.js';
8
- export { mountView, view, ViewBinding } from './dom.js';
7
+ export { effectEvent, makeEffectHandler } from './effectEvent.js';
8
+ export { mountView, view, list, listView, memoView, observe, ViewBinding, } from './dom.js';
9
+ export { mount } from './render.js';
10
+ export { mapSource, fromStream, fromSubscriptionRef, fromAtom, projectionSource, } from './source.js';
9
11
  export { lazyView } from './lazy.js';
10
- export { Portal, domBinding, domMount } from './mount.js';
12
+ export { Portal, domBinding, domMount, makeDomMount, makeDomBinding, } from './mount.js';
11
13
  export { pages, } from './pages.js';
12
- export { program, mapTransition, actionCommand, effectCommand, commandSlot, } from './program.js';
14
+ export { program, makeProgram, mapTransition, actionCommand, effectCommand, commandSlot, } from './program.js';
13
15
  export { available, resourceComponent, resourceError, } from './resource.js';
14
16
  export { fromPromise } from './load.js';
15
17
  export { observeBindings, inspectBindings, mountBindingInspector, observePrograms, } from './diagnostics.js';
@@ -17,9 +19,9 @@ export { inputText, inputChecked, inputNumber, submit, defineField, } from './fo
17
19
  export { slot } from './dom.js';
18
20
  export { AsyncContent } from './AsyncContent.js';
19
21
  export { defineTasks, } from './tasks.js';
20
- export { uiRuntime } from './runtime.js';
21
- export { query, queryGroup } from './query.js';
22
- export { makeQueryCache } from './cache.js';
22
+ export { uiRuntime, makeUiRuntime } from './runtime.js';
23
+ export { query, queryGroup, } from './query.js';
24
+ export { makeQueryCache, scopedQueryCache, } from './cache.js';
23
25
  export { queryResource, observeQuery, lifetime, projectionCache, sessionGroup, } from './session.js';
24
26
  export { httpJson } from './http.js';
25
27
  export { infiniteQuery, infiniteResource, } from './infinite-query.js';
@@ -1,5 +1,6 @@
1
- import { Effect } from 'effect';
2
- import { type Query, type QueryArgs, type QueryGroup } from './query.js';
1
+ import * as Effect from 'effect/Effect';
2
+ import type * as Scope from 'effect/Scope';
3
+ import { type Query, type QueryKey, type QueryArgs, type QueryGroup } from './query.js';
3
4
  import type { QueryCache } from './cache.js';
4
5
  import { type QueryResource } from './session.js';
5
6
  import { type Snapshot } from './snapshot.js';
@@ -18,20 +19,23 @@ export interface InfiniteQuery<Args, A, Param, E = never, R = never> {
18
19
  }, A, E, R>;
19
20
  readonly maxPages: number;
20
21
  readonly next: (value: Snapshot<A>, param: Snapshot<Param>) => Param | undefined;
22
+ readonly paramKey: (param: Param | Snapshot<Param> | undefined) => string;
21
23
  }
22
24
  /** Shared pages with explicit cursor identity. Refresh preserves retained page parameters by default. */
23
25
  export declare function infiniteQuery<Args, A, Param, E = never, R = never>(definition: {
24
26
  readonly name: string;
25
27
  readonly initial: Param;
26
- readonly load: (args: Snapshot<Args>, param: Snapshot<Param>) => Effect.Effect<A, E, R>;
28
+ readonly load: (args: Snapshot<Args>, param: Snapshot<Param>) => Effect.Effect<A, E, R | Scope.Scope>;
27
29
  readonly next: (value: Snapshot<A>, param: Snapshot<Param>) => Param | undefined;
28
30
  readonly maxPages?: number;
29
31
  readonly refresh?: 'retained' | 'first';
30
32
  readonly groups?: readonly QueryGroup[];
31
- } & (Args extends QueryArgs<Args> ? unknown : {
32
- readonly nonSerializableQueryArguments: never;
33
- }) & (Param extends QueryArgs<Param> ? unknown : {
34
- readonly nonSerializablePageParameters: never;
33
+ readonly encodeArgs?: (args: Snapshot<Args>) => QueryKey;
34
+ readonly encodeParam?: (param: Snapshot<Param>) => QueryKey;
35
+ } & ([Args] extends [QueryArgs<Args>] ? unknown : {
36
+ readonly encodeArgs: (args: Snapshot<Args>) => QueryKey;
37
+ }) & ([Param] extends [QueryArgs<Param>] ? unknown : {
38
+ readonly encodeParam: (param: Snapshot<Param>) => QueryKey;
35
39
  })): InfiniteQuery<Args, A, Param, E, R>;
36
40
  export interface InfiniteResource<Args, A, Param, E = never> extends QueryResource<Args, InfiniteData<A, Param>, E> {
37
41
  loadNext(): Effect.Effect<Snapshot<InfiniteData<A, Param>>, E>;
@@ -39,4 +43,4 @@ export interface InfiniteResource<Args, A, Param, E = never> extends QueryResour
39
43
  seed(args: Args | Snapshot<Args>, data: InfiniteData<A, Param> | Snapshot<InfiniteData<A, Param>>): Snapshot<InfiniteData<A, Param>>;
40
44
  }
41
45
  /** Cache-owned results, observer-owned subscriptions. Operations return typed Effects for task composition. */
42
- export declare function infiniteResource<Args, A, Param, E, R>(cache: QueryCache<R>, definition: InfiniteQuery<Args, A, Param, E, NoInfer<R>>): InfiniteResource<Args, A, Param, E>;
46
+ export declare function infiniteResource<Args, A, Param, E, R>(cache: QueryCache<R>, definition: InfiniteQuery<Args, A, Param, E, NoInfer<R> | Scope.Scope>): InfiniteResource<Args, A, Param, E>;
@@ -1,5 +1,5 @@
1
- import { Effect } from 'effect';
2
- import { query, encodeQueryKey, } from './query.js';
1
+ import * as Effect from 'effect/Effect';
2
+ import { query, encodeQueryKey, encodeQueryArguments, } from './query.js';
3
3
  import { cacheInternals } from './cache-internals.js';
4
4
  import { queryResource } from './session.js';
5
5
  import { protectSnapshot } from './snapshot.js';
@@ -9,19 +9,27 @@ export function infiniteQuery(definition) {
9
9
  const maxPages = config.maxPages ?? Infinity;
10
10
  if (maxPages !== Infinity && (!Number.isInteger(maxPages) || maxPages < 1))
11
11
  throw new RangeError('maxPages must be a positive integer.');
12
- encodeQueryKey(config.initial);
12
+ const encodeArgs = (args) => config.encodeArgs ? config.encodeArgs(args) : args;
13
+ const encodeParam = (param) => config.encodeParam ? config.encodeParam(param) : param;
14
+ const paramKey = (param) => encodeQueryKey(param === undefined ? undefined : encodeParam(param));
15
+ paramKey(config.initial);
13
16
  const initial = protectSnapshot(config.initial);
14
17
  const next = (value, param) => {
15
18
  const result = config.next(value, param);
16
- encodeQueryKey(result);
19
+ paramKey(result);
17
20
  return result;
18
21
  };
19
22
  const page = query({
20
23
  name: `${config.name}:page`,
24
+ encode: ({ args, param }) => ({
25
+ args: encodeArgs(args),
26
+ param: encodeParam(param),
27
+ }),
21
28
  load: ({ args, param }) => config.load(args, param),
22
29
  });
23
30
  const root = query({
24
31
  name: config.name,
32
+ encode: encodeArgs,
25
33
  ...(config.groups ? { groups: config.groups } : {}),
26
34
  load: (args, previous) => {
27
35
  const params = config.refresh !== 'first' && previous?.pages.length
@@ -36,7 +44,7 @@ export function infiniteQuery(definition) {
36
44
  }));
37
45
  },
38
46
  });
39
- return Object.freeze({ query: root, page, maxPages, next });
47
+ return Object.freeze({ query: root, page, maxPages, next, paramKey });
40
48
  }
41
49
  const operations = new WeakMap();
42
50
  function operationBook(cache, definition) {
@@ -70,11 +78,11 @@ export function infiniteResource(cache, definition) {
70
78
  const param = retry ? retry.param : current.next;
71
79
  if (param === undefined)
72
80
  return current;
73
- const pageKey = encodeQueryKey(param);
74
- const retained = current.pages.some((page) => encodeQueryKey(page.param) === pageKey);
75
- if (retry && !retained && encodeQueryKey(current.next) !== pageKey)
81
+ const pageKey = definition.paramKey(param);
82
+ const retained = current.pages.some((page) => definition.paramKey(page.param) === pageKey);
83
+ if (retry && !retained && definition.paramKey(current.next) !== pageKey)
76
84
  return yield* Effect.die(new RangeError('Retry a retained page or the next page parameter.'));
77
- const key = encodeQueryKey(args);
85
+ const key = encodeQueryArguments(definition.query, args);
78
86
  const revision = internal.revision(definition.query, args);
79
87
  let operation = book.get(key);
80
88
  if (!operation || operation.revision !== revision) {
@@ -95,8 +103,8 @@ export function infiniteResource(cache, definition) {
95
103
  book.get(key) !== state ||
96
104
  state.revision !== internal.revision(definition.query, args))
97
105
  return;
98
- const index = latest.pages.findIndex((page) => encodeQueryKey(page.param) === pageKey);
99
- if (index < 0 && (retained || encodeQueryKey(latest.next) !== pageKey))
106
+ const index = latest.pages.findIndex((page) => definition.paramKey(page.param) === pageKey);
107
+ if (index < 0 && (retained || definition.paramKey(latest.next) !== pageKey))
100
108
  return;
101
109
  if (index >= 0 && latest.pages[index].value === value)
102
110
  return;
@@ -134,8 +142,8 @@ export function infiniteResource(cache, definition) {
134
142
  seed(args, data) {
135
143
  if (!data.pages.length || data.pages.length > definition.maxPages)
136
144
  throw new RangeError('Seed pages must fit the retained page range.');
137
- encodeQueryKey(data.next);
138
- const keys = data.pages.map((page) => encodeQueryKey(page.param));
145
+ definition.paramKey(data.next);
146
+ const keys = data.pages.map((page) => definition.paramKey(page.param));
139
147
  if (new Set(keys).size !== keys.length)
140
148
  throw new TypeError('Seed page parameters must be unique.');
141
149
  return cache.setQueryData(definition.query, args, data);
@@ -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
@@ -4,6 +4,8 @@ import type { SnapshotOpaque } from './snapshot.js';
4
4
  import type { EffectEventRequest } from './effectEvent.js';
5
5
  import type { CompiledContent } from './dom.js';
6
6
  import type { DomMount } from './mount.js';
7
+ import type * as Effect from 'effect/Effect';
8
+ import type * as Scope from 'effect/Scope';
7
9
  /** Nominal eligibility for compiled JSX components and compiler primitives. */
8
10
  export declare const jsxComponent: unique symbol;
9
11
  /** JSX is a compiler input. No renderer library owns its types. */
@@ -16,10 +18,11 @@ export declare namespace JSX {
16
18
  export interface ElementArray extends ReadonlyArray<Element>, SnapshotOpaque {
17
19
  }
18
20
  export type Element = string | number | boolean | null | undefined | CompiledContent | ElementArray;
21
+ export type EventResult = void | boolean | EffectEventRequest | Effect.Effect<unknown, unknown, Scope.Scope>;
19
22
  export type EventHandler<T, E extends Event> = (event: E & {
20
23
  currentTarget: T;
21
24
  target: EventTarget & globalThis.Element;
22
- }) => void | boolean | EffectEventRequest;
25
+ }) => EventResult;
23
26
  export type EventHandlerUnion<T, E extends Event> = EventHandler<T, E>;
24
27
  export type CSSProperties = {
25
28
  [property: string]: string | number | undefined | null;
@@ -1,4 +1,6 @@
1
- import { Cause, Effect } from 'effect';
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Effect from 'effect/Effect';
3
+ import type * as Scope from 'effect/Scope';
2
4
  import { type TaskPolicy } from './program.js';
3
5
  import type { DisposableOwner } from './owner.js';
4
6
  import { type UiRuntime } from './runtime.js';
@@ -13,17 +15,17 @@ export type TaskOutcome<A, E> = {
13
15
  readonly _tag: 'Cancelled' | 'Superseded' | 'Dropped';
14
16
  };
15
17
  export interface TaskHandle<A, E> {
16
- readonly outcome: Promise<TaskOutcome<A, E>>;
18
+ readonly outcome: Effect.Effect<TaskOutcome<A, E>>;
17
19
  }
18
20
  /** Per-entity serialization. Inputs are captured now; run reads services when work starts. */
19
21
  export declare function keyedTasks<Key, Input, A, E = never, R = never>(owner: DisposableOwner, definition: {
20
22
  readonly name: string;
21
23
  readonly policy: TaskPolicy;
22
- readonly run: (key: Key, input: Snapshot<Input>) => Effect.Effect<A, E, R>;
23
- }, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): {
24
+ readonly run: (key: Key, input: Snapshot<Input>) => Effect.Effect<A, E, R | Scope.Scope>;
25
+ }, ...provided: [Exclude<R, Scope.Scope>] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): {
24
26
  submit(key: Key, input: Input | Snapshot<Input>): TaskHandle<A, E>;
25
27
  cancel(key: Key): void;
26
- drain(...selected: [] | [Key]): Promise<void>;
27
- close: () => Promise<void>;
28
+ drain(...selected: [] | [Key]): Effect.Effect<void>;
29
+ close: () => Effect.Effect<void, never, never>;
28
30
  dispose: () => void;
29
31
  };
@@ -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,40 +30,43 @@ 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));
63
- 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)
65
+ })).addObserver((exit) => {
66
+ if (Exit.isFailure(exit))
67
+ reportSafely(reportError, exit.cause);
68
+ });
69
+ const action = Effect.uninterruptibleMask((restore) => restore(Effect.scoped(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)
66
72
  ? { _tag: 'Cancelled' }
@@ -69,7 +75,7 @@ export function keyedTasks(owner, definition, ...provided) {
69
75
  command: {
70
76
  slot,
71
77
  policy: definition.policy,
72
- action: runtime.provide(action),
78
+ action: runtime.provideScoped(action),
73
79
  onDiscard: (_tag) => settle({ _tag }),
74
80
  },
75
81
  });
@@ -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,6 @@
1
- import { Effect, type Cause } from 'effect';
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Effect from 'effect/Effect';
3
+ import type * as Scope from 'effect/Scope';
2
4
  import { type View } from './dom.js';
3
5
  import { type UiRuntime } from './runtime.js';
4
6
  export interface LazyViewOptions<Model, Message, E> {
@@ -12,7 +14,7 @@ export interface LazyViewOptions<Model, Message, E> {
12
14
  * Mount-owned loading with current inputs and a reusable successful definition.
13
15
  * Unresolved placements own independent requests; unmount interrupts their work.
14
16
  */
15
- export declare function lazyView<Model, Message = never, E = never, R = never>(load: () => Effect.Effect<View<Model, Message>, E, R>, ...provided: [R] extends [never] ? [
17
+ export declare function lazyView<Model, Message = never, E = never, R = never>(load: () => Effect.Effect<View<Model, Message>, E, R | Scope.Scope>, ...provided: [Exclude<R, Scope.Scope>] extends [never] ? [
16
18
  options?: LazyViewOptions<NoInfer<Model>, NoInfer<Message>, NoInfer<E>> & {
17
19
  readonly runtime?: UiRuntime<R>;
18
20
  }
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';
@@ -8,9 +10,10 @@ import { defaultUiRuntime } from './runtime.js';
8
10
  */
9
11
  export function lazyView(load, ...provided) {
10
12
  const options = provided[0];
11
- const runtime = options?.runtime ?? defaultUiRuntime;
12
13
  let loaded;
13
14
  return compiled((scope, parent, before) => {
15
+ const ownerRuntime = scope.settlement.runtime ?? defaultUiRuntime;
16
+ const runtime = options?.runtime ?? ownerRuntime;
14
17
  const render = viewRegion(scope, parent, before);
15
18
  if (loaded) {
16
19
  render(loaded);
@@ -27,21 +30,19 @@ export function lazyView(load, ...provided) {
27
30
  return;
28
31
  const finished = scope.settlement.begin();
29
32
  try {
30
- fiber = Effect.runFork(runtime.provide(Effect.suspend(load)));
31
- fiber.addObserver(finished);
33
+ fiber = ownerRuntime.runFork(runtime.provideScoped(Effect.suspend(load)));
32
34
  }
33
35
  catch (error) {
34
36
  finished();
35
37
  throw error;
36
38
  }
37
- if (scope.disposed) {
38
- interrupt();
39
- return;
40
- }
41
39
  fiber.addObserver((exit) => {
42
- if (scope.disposed)
43
- return;
44
40
  try {
41
+ if (scope.disposed) {
42
+ if (exit._tag === 'Failure' && !Cause.hasInterruptsOnly(exit.cause))
43
+ reportSafely(scope.report, exit.cause);
44
+ return;
45
+ }
45
46
  if (exit._tag === 'Success') {
46
47
  loaded = exit.value;
47
48
  render(loaded);
@@ -61,6 +62,11 @@ export function lazyView(load, ...provided) {
61
62
  catch (error) {
62
63
  reportSafely(scope.report, error);
63
64
  }
65
+ finally {
66
+ finished();
67
+ }
64
68
  });
69
+ if (scope.disposed)
70
+ interrupt();
65
71
  });
66
72
  }
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 });