effectweb 0.1.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/AsyncContent.d.ts +20 -0
  4. package/dist/AsyncContent.js +75 -0
  5. package/dist/actions.d.ts +27 -0
  6. package/dist/actions.js +31 -0
  7. package/dist/cache.d.ts +32 -0
  8. package/dist/cache.js +147 -0
  9. package/dist/collection.d.ts +15 -0
  10. package/dist/collection.js +24 -0
  11. package/dist/component.d.ts +33 -0
  12. package/dist/component.js +58 -0
  13. package/dist/diagnostics.d.ts +32 -0
  14. package/dist/diagnostics.js +57 -0
  15. package/dist/dom.d.ts +66 -0
  16. package/dist/dom.js +617 -0
  17. package/dist/effectEvent.d.ts +18 -0
  18. package/dist/effectEvent.js +67 -0
  19. package/dist/errors.d.ts +5 -0
  20. package/dist/errors.js +23 -0
  21. package/dist/form.d.ts +19 -0
  22. package/dist/form.js +13 -0
  23. package/dist/index.d.ts +22 -0
  24. package/dist/index.js +21 -0
  25. package/dist/jsx-runtime.d.ts +1 -0
  26. package/dist/jsx-runtime.js +1 -0
  27. package/dist/jsx.d.ts +86 -0
  28. package/dist/jsx.js +1 -0
  29. package/dist/load.d.ts +13 -0
  30. package/dist/load.js +11 -0
  31. package/dist/mount.d.ts +25 -0
  32. package/dist/mount.js +61 -0
  33. package/dist/pages.d.ts +40 -0
  34. package/dist/pages.js +86 -0
  35. package/dist/program.d.ts +48 -0
  36. package/dist/program.js +154 -0
  37. package/dist/query.d.ts +22 -0
  38. package/dist/query.js +12 -0
  39. package/dist/resource.d.ts +34 -0
  40. package/dist/resource.js +59 -0
  41. package/dist/runtime.d.ts +16 -0
  42. package/dist/runtime.js +27 -0
  43. package/dist/session.d.ts +65 -0
  44. package/dist/session.js +186 -0
  45. package/dist/share.d.ts +2 -0
  46. package/dist/share.js +40 -0
  47. package/dist/state.d.ts +2 -0
  48. package/dist/state.js +7 -0
  49. package/dist/task.d.ts +53 -0
  50. package/dist/task.js +70 -0
  51. package/dist/tasks.d.ts +69 -0
  52. package/dist/tasks.js +126 -0
  53. package/dist/testing.d.ts +26 -0
  54. package/dist/testing.js +44 -0
  55. package/package.json +137 -0
@@ -0,0 +1,22 @@
1
+ import type { Effect } from 'effect';
2
+ /** One logical read. Share its definition between consumers, prefetching and mutations. */
3
+ export interface Query<Args, A, E = never, R = never> {
4
+ readonly id: number;
5
+ readonly name: string;
6
+ readonly key: (args: Args) => string;
7
+ readonly load: (args: Args) => Effect.Effect<A, E, R>;
8
+ /** Revalidate on activation/prefetch after this many milliseconds. No polling timer. */
9
+ readonly staleTime: number;
10
+ }
11
+ /** Identity belongs to arguments, never callback identity. Each application cache owns account isolation. */
12
+ export declare function query<A, E = never, R = never>(definition: {
13
+ readonly name: string;
14
+ readonly load: () => Effect.Effect<A, E, R>;
15
+ readonly staleTime?: number;
16
+ }): Query<true, A, E, R>;
17
+ export declare function query<Args, A, E = never, R = never>(definition: {
18
+ readonly name: string;
19
+ readonly key: (args: Args) => string;
20
+ readonly load: (args: Args) => Effect.Effect<A, E, R>;
21
+ readonly staleTime?: number;
22
+ }): Query<Args, A, E, R>;
package/dist/query.js ADDED
@@ -0,0 +1,12 @@
1
+ let nextQueryId = 0;
2
+ export function query(definition) {
3
+ const staleTime = definition.staleTime ?? Infinity;
4
+ if (Number.isNaN(staleTime) || staleTime < 0)
5
+ throw new RangeError('Query staleTime must be nonnegative.');
6
+ return Object.freeze({
7
+ ...definition,
8
+ key: definition.key ?? (() => ''),
9
+ id: ++nextQueryId,
10
+ staleTime,
11
+ });
12
+ }
@@ -0,0 +1,34 @@
1
+ import { Cause } from 'effect';
2
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
+ import type { UiLoad } from './load.js';
4
+ import { type UiRuntime } from './runtime.js';
5
+ import type { View } from './dom.js';
6
+ export interface ResourceModel<Props, A, E = unknown> {
7
+ readonly props: Props;
8
+ readonly key: string | undefined;
9
+ readonly result: AsyncResult.AsyncResult<A, E>;
10
+ }
11
+ export type ResourceMessage<A, E = unknown> = {
12
+ type: 'Retry';
13
+ } | {
14
+ type: 'Loaded';
15
+ value: A;
16
+ } | {
17
+ type: 'Failed';
18
+ cause: Cause.Cause<E>;
19
+ };
20
+ export declare const available: <A>(result: AsyncResult.AsyncResult<A, unknown>) => A | undefined;
21
+ export declare const resourceError: (result: AsyncResult.AsyncResult<unknown, unknown>) => string;
22
+ /** A replaceable request owns one result. Key changes clear it; refreshes preserve the last success. */
23
+ export declare function resourceComponent<Props, A, E = unknown, R = never>(definition: {
24
+ request: (props: Props) => {
25
+ key: string;
26
+ load: () => UiLoad<A, E, R>;
27
+ delay?: number;
28
+ } | undefined;
29
+ view: View<ResourceModel<Props, A, E>, ResourceMessage<A, E>>;
30
+ } & ([R] extends [never] ? {
31
+ runtime?: UiRuntime<R>;
32
+ } : {
33
+ runtime: UiRuntime<R>;
34
+ })): View<Props, never>;
@@ -0,0 +1,59 @@
1
+ import { Cause, Effect, Option } from 'effect';
2
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
+ import { loadEffect } from './load.js';
4
+ import { component } from './component.js';
5
+ import { defaultUiRuntime } from './runtime.js';
6
+ import { effectCommand } from './program.js';
7
+ export const available = (result) => Option.getOrUndefined(AsyncResult.value(result));
8
+ export const resourceError = (result) => AsyncResult.isFailure(result) ? String(Cause.squash(result.cause)) : '';
9
+ /** A replaceable request owns one result. Key changes clear it; refreshes preserve the last success. */
10
+ export function resourceComponent(definition) {
11
+ const runtime = definition.runtime ?? defaultUiRuntime;
12
+ const request = (model, retry = false) => {
13
+ const selected = definition.request(model.props);
14
+ if (!retry && selected?.key === model.key)
15
+ return { model };
16
+ if (!selected)
17
+ return {
18
+ model: { ...model, key: undefined, result: AsyncResult.initial() },
19
+ cancel: ['load'],
20
+ };
21
+ return {
22
+ model: {
23
+ ...model,
24
+ key: selected.key,
25
+ result: AsyncResult.waiting(selected.key === model.key ? model.result : AsyncResult.initial()),
26
+ },
27
+ commands: [
28
+ effectCommand('load', () => runtime.provide(selected.delay
29
+ ? Effect.sleep(selected.delay).pipe(Effect.andThen(loadEffect(selected.load)))
30
+ : loadEffect(selected.load)), {
31
+ onSuccess: (value) => ({ type: 'Loaded', value }),
32
+ onFailure: (cause) => ({ type: 'Failed', cause }),
33
+ }),
34
+ ],
35
+ };
36
+ };
37
+ return component({
38
+ init: (props) => ({ props, key: undefined, result: AsyncResult.initial() }),
39
+ receive: (model, props) => request({ ...model, props }),
40
+ update: (model, message) => {
41
+ switch (message.type) {
42
+ case 'Retry':
43
+ return request(model, true);
44
+ case 'Loaded':
45
+ return { model: { ...model, result: AsyncResult.success(message.value) } };
46
+ case 'Failed':
47
+ return {
48
+ model: {
49
+ ...model,
50
+ result: AsyncResult.failureWithPrevious(message.cause, {
51
+ previous: Option.some(model.result),
52
+ }),
53
+ },
54
+ };
55
+ }
56
+ },
57
+ view: definition.view,
58
+ });
59
+ }
@@ -0,0 +1,16 @@
1
+ import { Context, Effect } from 'effect';
2
+ import { type Command, type RunningProgram, type Transition } from './program.js';
3
+ /** A binding to application-owned services, not a new scope or service lifetime. */
4
+ export interface UiRuntime<R> {
5
+ readonly context: Context.Context<R>;
6
+ readonly provide: <A, E>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E>;
7
+ readonly command: <M>(command: Command<M, R>) => Command<M>;
8
+ readonly program: <M, Msg>(options: {
9
+ initial: M;
10
+ update: (model: M, message: Msg) => Transition<M, Msg, R>;
11
+ name?: string;
12
+ onDefect?: (cause: unknown) => void;
13
+ }) => RunningProgram<M, Msg>;
14
+ }
15
+ export declare function uiRuntime<R>(context: Context.Context<R>): UiRuntime<R>;
16
+ export declare const defaultUiRuntime: UiRuntime<never>;
@@ -0,0 +1,27 @@
1
+ import { Context, Effect, Stream } from 'effect';
2
+ import { program } from './program.js';
3
+ export function uiRuntime(context) {
4
+ const provide = (effect) => Effect.provideContext(effect, context);
5
+ const command = (value) => value.stream
6
+ ? { slot: value.slot, stream: value.stream.pipe(Stream.provideContext(context)) }
7
+ : value.action
8
+ ? { slot: value.slot, action: provide(value.action) }
9
+ : { slot: value.slot, effect: provide(value.effect) };
10
+ return {
11
+ context,
12
+ provide,
13
+ command,
14
+ program: (options) => program({
15
+ ...options,
16
+ update: (model, message) => {
17
+ const next = options.update(model, message);
18
+ return {
19
+ model: next.model,
20
+ ...(next.cancel ? { cancel: next.cancel } : {}),
21
+ ...(next.commands ? { commands: next.commands.map(command) } : {}),
22
+ };
23
+ },
24
+ }),
25
+ };
26
+ }
27
+ export const defaultUiRuntime = uiRuntime(Context.empty());
@@ -0,0 +1,65 @@
1
+ import type { Query } from './query.js';
2
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
+ import type { UiLoad, UiModel } from './cache.js';
4
+ import { type UiPage } from './cache.js';
5
+ /** Explicit ownership for the application's DOM and Telegram adapters. */
6
+ export declare function lifetime(): {
7
+ add(cleanup: () => void): void;
8
+ dispose(): void;
9
+ readonly disposed: boolean;
10
+ };
11
+ export type Read<A> = () => A;
12
+ export interface SessionContext<R = never> {
13
+ readonly cache: UiModel<R>;
14
+ readonly changed: () => void;
15
+ }
16
+ /** Explicit request selection. Reads are Effect values; no tracking context or reactive props. */
17
+ export declare function resource<A>(context: SessionContext, namespace?: string): {
18
+ select(selected: string | undefined, load: () => UiLoad<A>): void;
19
+ read: () => AsyncResult.AsyncResult<A, unknown>;
20
+ refresh: () => void;
21
+ dispose: () => void;
22
+ };
23
+ /**
24
+ * Idempotent query reconciliation for snapshot ticks. Freshness is checked when entering a
25
+ * selection (including remount), not on every same-key tick; refresh explicitly revalidates.
26
+ */
27
+ export declare function queryResource<Args, A, E, R>(context: SessionContext<R>, definition: Query<Args, A, E, NoInfer<R>>): {
28
+ select(args: Args | undefined): void;
29
+ read: () => AsyncResult.AsyncResult<A, E>;
30
+ refresh: () => void;
31
+ dispose: () => void;
32
+ };
33
+ export declare function pagedResource<A, Cursor>(context: SessionContext, itemKey: (item: A) => string): {
34
+ select(selected: string | undefined, load: (cursor: Cursor | undefined) => UiLoad<UiPage<A, Cursor>>): void;
35
+ snapshot(): {
36
+ items: A[];
37
+ totalCount: number | undefined;
38
+ loading: boolean;
39
+ loadingMore: boolean;
40
+ hasMore: boolean;
41
+ error: string;
42
+ };
43
+ more: () => void | undefined;
44
+ refresh: () => void | undefined;
45
+ dispose: () => void;
46
+ };
47
+ /** Application selectors run once per explicit input version, with optional prior output sharing. */
48
+ export declare function projectionCache(): {
49
+ invalidate: () => void;
50
+ select: {
51
+ <A>(compute: (previous: A) => A, initial: A): () => A;
52
+ <A>(compute: (previous: A | undefined) => A): () => A;
53
+ };
54
+ };
55
+ interface OwnedSession {
56
+ dispose(): void;
57
+ refresh?(): void;
58
+ subscribe?(changed: () => void): () => void;
59
+ }
60
+ /** One declaration owns subscriptions, refresh order and disposal for a group of sessions. */
61
+ export declare function sessionGroup(sessions: readonly OwnedSession[], changed: () => void): {
62
+ refresh: () => void;
63
+ dispose: () => void;
64
+ };
65
+ export {};
@@ -0,0 +1,186 @@
1
+ import { runAll } from './errors.js';
2
+ import { Cause, Option } from 'effect';
3
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
4
+ import * as Atom from 'effect/unstable/reactivity/Atom';
5
+ import { loadEffect, makePagedResource } from './cache.js';
6
+ /** Explicit ownership for the application's DOM and Telegram adapters. */
7
+ export function lifetime() {
8
+ const cleanups = [];
9
+ let disposed = false;
10
+ return {
11
+ add(cleanup) {
12
+ if (disposed)
13
+ cleanup();
14
+ else
15
+ cleanups.push(cleanup);
16
+ },
17
+ dispose() {
18
+ if (disposed)
19
+ return;
20
+ disposed = true;
21
+ runAll(cleanups.splice(0).reverse());
22
+ },
23
+ get disposed() {
24
+ return disposed;
25
+ },
26
+ };
27
+ }
28
+ /** Explicit request selection. Reads are Effect values; no tracking context or reactive props. */
29
+ export function resource(context, namespace) {
30
+ let key, atom;
31
+ let stop;
32
+ const initial = AsyncResult.initial();
33
+ return {
34
+ select(selected, load) {
35
+ const next = selected === undefined
36
+ ? undefined
37
+ : `${context.cache.registry.get(context.cache.generation)}:${selected}`;
38
+ if (next === key)
39
+ return;
40
+ stop?.();
41
+ stop = undefined;
42
+ key = next;
43
+ atom =
44
+ next === undefined
45
+ ? undefined
46
+ : namespace
47
+ ? context.cache.resource(`${namespace}:${selected}`, load)
48
+ : Atom.make(loadEffect(load)).pipe(Atom.setIdleTTL(0));
49
+ if (atom)
50
+ stop = context.cache.registry.subscribe(atom, context.changed, { immediate: true });
51
+ context.changed();
52
+ },
53
+ read: () => (atom ? context.cache.registry.get(atom) : initial),
54
+ refresh: () => {
55
+ if (atom)
56
+ context.cache.registry.refresh(atom);
57
+ },
58
+ dispose: () => {
59
+ stop?.();
60
+ stop = undefined;
61
+ atom = undefined;
62
+ key = undefined;
63
+ },
64
+ };
65
+ }
66
+ /**
67
+ * Idempotent query reconciliation for snapshot ticks. Freshness is checked when entering a
68
+ * selection (including remount), not on every same-key tick; refresh explicitly revalidates.
69
+ */
70
+ export function queryResource(context, definition) {
71
+ let key;
72
+ let generation = -1;
73
+ let atom;
74
+ let stop;
75
+ let disposed = false;
76
+ const initial = AsyncResult.initial();
77
+ return {
78
+ select(args) {
79
+ if (disposed)
80
+ return;
81
+ const nextGeneration = context.cache.registry.get(context.cache.generation);
82
+ const nextKey = args === undefined ? undefined : definition.key(args);
83
+ if (nextKey === key && nextGeneration === generation)
84
+ return;
85
+ stop?.();
86
+ stop = undefined;
87
+ key = nextKey;
88
+ generation = nextGeneration;
89
+ atom = args === undefined ? undefined : context.cache.query(definition, args);
90
+ if (atom)
91
+ stop = context.cache.registry.subscribe(atom, context.changed, { immediate: true });
92
+ context.changed();
93
+ },
94
+ read: () => atom && !disposed && generation === context.cache.registry.get(context.cache.generation)
95
+ ? context.cache.registry.get(atom)
96
+ : initial,
97
+ refresh: () => {
98
+ if (atom && !disposed)
99
+ context.cache.registry.refresh(atom);
100
+ },
101
+ dispose: () => {
102
+ disposed = true;
103
+ stop?.();
104
+ stop = undefined;
105
+ atom = undefined;
106
+ key = undefined;
107
+ },
108
+ };
109
+ }
110
+ export function pagedResource(context, itemKey) {
111
+ let key, page, stop;
112
+ const initial = AsyncResult.initial();
113
+ const state = () => (page ? context.cache.registry.get(page.atom) : initial);
114
+ return {
115
+ select(selected, load) {
116
+ const next = selected === undefined
117
+ ? undefined
118
+ : `${context.cache.registry.get(context.cache.generation)}:${selected}`;
119
+ if (next === key)
120
+ return;
121
+ stop?.();
122
+ stop = undefined;
123
+ key = next;
124
+ page =
125
+ next === undefined ? undefined : makePagedResource(context.cache.registry, load, itemKey);
126
+ if (page)
127
+ stop = context.cache.registry.subscribe(page.atom, context.changed, { immediate: true });
128
+ context.changed();
129
+ },
130
+ snapshot() {
131
+ const result = state(), value = Option.getOrUndefined(AsyncResult.value(result));
132
+ return {
133
+ items: value?.items ?? [],
134
+ totalCount: value?.totalCount,
135
+ loading: result.waiting && !value,
136
+ loadingMore: result.waiting && Boolean(value),
137
+ hasMore: value?.next !== undefined,
138
+ error: AsyncResult.isFailure(result) ? String(Cause.squash(result.cause)) : '',
139
+ };
140
+ },
141
+ more: () => page?.more(),
142
+ refresh: () => page?.refresh(),
143
+ dispose: () => {
144
+ stop?.();
145
+ stop = undefined;
146
+ page = undefined;
147
+ key = undefined;
148
+ },
149
+ };
150
+ }
151
+ /** Application selectors run once per explicit input version, with optional prior output sharing. */
152
+ export function projectionCache() {
153
+ let version = 0;
154
+ function select(compute, initial) {
155
+ let seen = -1, value = initial;
156
+ return () => {
157
+ if (seen !== version) {
158
+ value = compute(value);
159
+ seen = version;
160
+ }
161
+ return value;
162
+ };
163
+ }
164
+ return {
165
+ invalidate: () => {
166
+ version++;
167
+ },
168
+ select,
169
+ };
170
+ }
171
+ /** One declaration owns subscriptions, refresh order and disposal for a group of sessions. */
172
+ export function sessionGroup(sessions, changed) {
173
+ const scope = lifetime();
174
+ for (const session of sessions) {
175
+ scope.add(() => session.dispose());
176
+ if (session.subscribe)
177
+ scope.add(session.subscribe(changed));
178
+ }
179
+ return {
180
+ refresh: () => {
181
+ if (!scope.disposed)
182
+ runAll(sessions.map((session) => () => session.refresh?.()));
183
+ },
184
+ dispose: () => scope.dispose(),
185
+ };
186
+ }
@@ -0,0 +1,2 @@
1
+ /** Share JSON-shaped data; Blob, typed arrays and class instances retain their own identity. */
2
+ export declare function shareValue<T>(previous: T, next: T): T;
package/dist/share.js ADDED
@@ -0,0 +1,40 @@
1
+ // Immutable snapshots can reach the renderer through several projections. Reuse an
2
+ // already compared object pair without retaining either snapshot after its owners release it.
3
+ const sharedPairs = new WeakMap();
4
+ /** Share JSON-shaped data; Blob, typed arrays and class instances retain their own identity. */
5
+ export function shareValue(previous, next) {
6
+ if (Object.is(previous, next))
7
+ return previous;
8
+ if (!previous || !next || typeof previous !== 'object' || typeof next !== 'object')
9
+ return next;
10
+ const array = Array.isArray(next);
11
+ if (array !== Array.isArray(previous))
12
+ return next;
13
+ if (!array &&
14
+ (Object.getPrototypeOf(next) !== Object.prototype ||
15
+ Object.getPrototypeOf(previous) !== Object.prototype))
16
+ return next;
17
+ let pairs = sharedPairs.get(previous);
18
+ if (pairs?.has(next))
19
+ return pairs.get(next);
20
+ const old = previous;
21
+ const value = next;
22
+ const keys = Object.keys(value);
23
+ let equal = keys.length === Object.keys(old).length;
24
+ let unchangedNext = true;
25
+ const shared = array ? [] : {};
26
+ for (const key of keys) {
27
+ shared[key] = shareValue(old[key], value[key]);
28
+ if (!Object.hasOwn(old, key) || shared[key] !== old[key])
29
+ equal = false;
30
+ if (shared[key] !== value[key])
31
+ unchangedNext = false;
32
+ }
33
+ const result = equal ? previous : unchangedNext ? next : shared;
34
+ if (!pairs) {
35
+ pairs = new WeakMap();
36
+ sharedPairs.set(previous, pairs);
37
+ }
38
+ pairs.set(next, result);
39
+ return result;
40
+ }
@@ -0,0 +1,2 @@
1
+ /** Shallow immutable update; unchanged fields preserve identity and skip publication. */
2
+ export declare function patchModel<Model extends object>(model: Model, patch: Partial<NoInfer<Model>>): Model;
package/dist/state.js ADDED
@@ -0,0 +1,7 @@
1
+ /** Shallow immutable update; unchanged fields preserve identity and skip publication. */
2
+ export function patchModel(model, patch) {
3
+ return Reflect.ownKeys(patch).some((key) => Object.prototype.propertyIsEnumerable.call(patch, key) &&
4
+ (!Object.hasOwn(model, key) || !Object.is(model[key], patch[key])))
5
+ ? { ...model, ...patch }
6
+ : model;
7
+ }
package/dist/task.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { Effect } from 'effect';
2
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
+ import type { View } from './dom.js';
4
+ import { type Send } from './program.js';
5
+ import { type UiRuntime } from './runtime.js';
6
+ export type TaskModel<Props, State, A, E = unknown> = State & {
7
+ readonly props: Props;
8
+ readonly task: AsyncResult.AsyncResult<A, E>;
9
+ };
10
+ export type TaskMessage<State, Input> = {
11
+ type: 'Run';
12
+ input: Input;
13
+ } | {
14
+ type: 'Fields';
15
+ fields: Partial<State> & {
16
+ props?: never;
17
+ task?: never;
18
+ };
19
+ } | {
20
+ type: 'Cancel';
21
+ };
22
+ /**
23
+ * One owned async action and local editable fields; policy is part of its public behavior.
24
+ * Compatibility factory for existing single-task components. New components can use
25
+ * defineTasks(...).tasks(...).view(view(...)) to establish inference before JSX.
26
+ */
27
+ export declare function taskComponent<Props, State extends object, Input, A, E = unknown, R = never>(definition: {
28
+ init: (props: Props) => State & {
29
+ props?: never;
30
+ task?: never;
31
+ };
32
+ identity?: (props: Props) => unknown;
33
+ task: {
34
+ policy: 'drop' | 'replace';
35
+ run: (model: State & {
36
+ readonly props: Props;
37
+ }, input: Input) => Effect.Effect<A, E, R>;
38
+ };
39
+ view: View<TaskModel<Props, State, A, E>, TaskMessage<State, Input>>;
40
+ } & ([R] extends [never] ? {
41
+ runtime?: UiRuntime<R>;
42
+ } : {
43
+ runtime: UiRuntime<R>;
44
+ })): View<Props, never>;
45
+ /** Stable methods when bound once in a compiled view's const declaration. */
46
+ export declare function taskControls<State, Input>(send: Send<TaskMessage<State, Input>>): {
47
+ run: (input: Input) => void;
48
+ patch: (fields: Partial<State> & {
49
+ props?: never;
50
+ task?: never;
51
+ }) => void;
52
+ cancel: () => void;
53
+ };
package/dist/task.js ADDED
@@ -0,0 +1,70 @@
1
+ import { Cause, Effect, Option } from 'effect';
2
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
+ import { component } from './component.js';
4
+ import { effectCommand } from './program.js';
5
+ import { patchModel } from './state.js';
6
+ import { defaultUiRuntime } from './runtime.js';
7
+ /**
8
+ * One owned async action and local editable fields; policy is part of its public behavior.
9
+ * Compatibility factory for existing single-task components. New components can use
10
+ * defineTasks(...).tasks(...).view(view(...)) to establish inference before JSX.
11
+ */
12
+ export function taskComponent(definition) {
13
+ const runtime = definition.runtime ?? defaultUiRuntime;
14
+ const init = (props) => ({
15
+ ...definition.init(props),
16
+ props,
17
+ task: AsyncResult.initial(),
18
+ });
19
+ return component({
20
+ init,
21
+ receive: (model, props) => definition.identity &&
22
+ !Object.is(definition.identity(model.props), definition.identity(props))
23
+ ? { model: init(props), cancel: ['task'] }
24
+ : { model: Object.is(model.props, props) ? model : { ...model, props } },
25
+ update: (model, message) => {
26
+ switch (message.type) {
27
+ case 'Fields': {
28
+ const next = patchModel(model, message.fields);
29
+ return {
30
+ model: next === model ? model : { ...next, props: model.props, task: model.task },
31
+ };
32
+ }
33
+ case 'Run':
34
+ if (definition.task.policy === 'drop' && model.task.waiting)
35
+ return { model };
36
+ return {
37
+ model: { ...model, task: AsyncResult.waiting(model.task) },
38
+ commands: [
39
+ effectCommand('task', () => runtime.provide(definition.task.run(model, message.input)), {
40
+ onSuccess: (value) => ({ type: 'Succeeded', value }),
41
+ onFailure: (cause) => ({ type: 'Failed', cause }),
42
+ }),
43
+ ],
44
+ };
45
+ case 'Cancel':
46
+ return { model: { ...model, task: AsyncResult.initial() }, cancel: ['task'] };
47
+ case 'Succeeded':
48
+ return { model: { ...model, task: AsyncResult.success(message.value) } };
49
+ case 'Failed':
50
+ return {
51
+ model: {
52
+ ...model,
53
+ task: AsyncResult.failureWithPrevious(message.cause, {
54
+ previous: Option.some(model.task),
55
+ }),
56
+ },
57
+ };
58
+ }
59
+ },
60
+ view: definition.view,
61
+ });
62
+ }
63
+ /** Stable methods when bound once in a compiled view's const declaration. */
64
+ export function taskControls(send) {
65
+ return {
66
+ run: (input) => send({ type: 'Run', input }),
67
+ patch: (fields) => send({ type: 'Fields', fields }),
68
+ cancel: () => send({ type: 'Cancel' }),
69
+ };
70
+ }