effectweb 0.2.2 → 0.3.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 (65) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +94 -2
  3. package/dist/AsyncContent.d.ts +2 -2
  4. package/dist/AsyncContent.js +36 -24
  5. package/dist/actions.d.ts +4 -4
  6. package/dist/actions.js +1 -1
  7. package/dist/cache-internals.d.ts +14 -0
  8. package/dist/cache-internals.js +11 -0
  9. package/dist/cache.d.ts +17 -15
  10. package/dist/cache.js +98 -27
  11. package/dist/collection.d.ts +10 -10
  12. package/dist/collection.js +32 -6
  13. package/dist/component.d.ts +8 -7
  14. package/dist/diagnostics.d.ts +25 -0
  15. package/dist/diagnostics.js +167 -1
  16. package/dist/dom-attributes.json +71 -0
  17. package/dist/dom.d.ts +36 -13
  18. package/dist/dom.js +547 -84
  19. package/dist/form.d.ts +61 -0
  20. package/dist/form.js +98 -0
  21. package/dist/html-attributes.d.ts +602 -0
  22. package/dist/html-attributes.js +1 -0
  23. package/dist/http.d.ts +5 -0
  24. package/dist/http.js +7 -0
  25. package/dist/index.d.ts +12 -10
  26. package/dist/index.js +7 -5
  27. package/dist/jsx.d.ts +165 -33
  28. package/dist/jsx.js +2 -1
  29. package/dist/lazy.d.ts +23 -0
  30. package/dist/lazy.js +58 -0
  31. package/dist/load.d.ts +3 -4
  32. package/dist/mount.d.ts +8 -11
  33. package/dist/mount.js +1 -3
  34. package/dist/owner.d.ts +11 -9
  35. package/dist/owner.js +27 -38
  36. package/dist/pages.d.ts +23 -23
  37. package/dist/pages.js +15 -10
  38. package/dist/program.d.ts +19 -10
  39. package/dist/program.js +172 -66
  40. package/dist/query-internals.d.ts +11 -0
  41. package/dist/query-internals.js +10 -0
  42. package/dist/query.d.ts +24 -20
  43. package/dist/query.js +60 -7
  44. package/dist/resource.d.ts +4 -10
  45. package/dist/resource.js +14 -5
  46. package/dist/runtime.d.ts +1 -2
  47. package/dist/runtime.js +3 -3
  48. package/dist/session.d.ts +12 -18
  49. package/dist/session.js +15 -15
  50. package/dist/share.d.ts +5 -4
  51. package/dist/share.js +5 -44
  52. package/dist/sharing.d.ts +6 -0
  53. package/dist/sharing.js +93 -0
  54. package/dist/snapshot.d.ts +16 -5
  55. package/dist/snapshot.js +6 -7
  56. package/dist/state.d.ts +0 -1
  57. package/dist/state.js +0 -1
  58. package/dist/svg-attributes.d.ts +12 -0
  59. package/dist/svg-attributes.js +1 -0
  60. package/dist/task.d.ts +9 -8
  61. package/dist/task.js +75 -47
  62. package/dist/tasks.d.ts +20 -15
  63. package/dist/tasks.js +39 -25
  64. package/dist/testing.d.ts +10 -15
  65. package/package.json +5 -1
package/dist/task.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { commandSlot } from './program.js';
1
2
  import { Cause, Effect, Option } from 'effect';
2
3
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
- import { component } from './component.js';
4
- import { effectCommand } from './program.js';
4
+ import { programView } from './component.js';
5
+ import { effectCommand, } from './program.js';
5
6
  import { patchModel } from './state.js';
6
7
  import { defaultUiRuntime } from './runtime.js';
8
+ const commandTask = commandSlot('task');
7
9
  /**
8
10
  * One owned async action and local editable fields; policy is part of its public behavior.
9
11
  * Compatibility factory for existing single-task components. New components can use
@@ -11,52 +13,78 @@ import { defaultUiRuntime } from './runtime.js';
11
13
  */
12
14
  export function taskComponent(definition) {
13
15
  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
- }
16
+ const init = (props) => ({ ...definition.init(props), props, task: AsyncResult.initial() });
17
+ const owners = new WeakMap();
18
+ return programView({
19
+ create(props) {
20
+ const source = runtime.program({
21
+ initial: init(props),
22
+ update: (snapshot, message) => {
23
+ // The public task receives the immutable snapshot; reconstruction stays internal.
24
+ const model = snapshot;
25
+ switch (message.type) {
26
+ case 'Input':
27
+ return definition.identity &&
28
+ !Object.is(definition.identity(model.props), definition.identity(message.props))
29
+ ? { model: init(message.props), cancel: [commandTask] }
30
+ : {
31
+ model: Object.is(model.props, message.props)
32
+ ? model
33
+ : { ...model, props: message.props },
34
+ };
35
+ case 'Fields': {
36
+ const next = patchModel(model, message.fields);
37
+ return {
38
+ model: next === model ? model : { ...next, props: model.props, task: model.task },
39
+ };
40
+ }
41
+ case 'Run':
42
+ if (definition.task.policy === 'drop' && model.task.waiting)
43
+ return { model };
44
+ return {
45
+ model: { ...model, task: AsyncResult.waiting(model.task) },
46
+ commands: [
47
+ {
48
+ ...effectCommand(commandTask, () => definition.task.run(snapshot, message.input), {
49
+ policy: definition.task.policy,
50
+ onSuccess: (value) => ({ type: 'Succeeded', value }),
51
+ onFailure: (cause) => ({ type: 'Failed', cause }),
52
+ }),
53
+ policy: definition.task.policy,
54
+ },
55
+ ],
56
+ };
57
+ case 'Cancel':
58
+ return {
59
+ model: { ...model, task: AsyncResult.initial() },
60
+ cancel: [commandTask],
61
+ };
62
+ case 'Succeeded':
63
+ return {
64
+ model: {
65
+ ...model,
66
+ task: AsyncResult.success(message.value, {
67
+ waiting: source.activeSlots().includes(commandTask),
68
+ }),
69
+ },
70
+ };
71
+ case 'Failed':
72
+ return {
73
+ model: {
74
+ ...model,
75
+ task: AsyncResult.failureWithPrevious(message.cause, {
76
+ previous: Option.some(model.task),
77
+ waiting: source.activeSlots().includes(commandTask),
78
+ }),
79
+ },
80
+ };
81
+ }
82
+ },
83
+ });
84
+ owners.set(source, source);
85
+ return source;
59
86
  },
87
+ receive: (source, props) => owners.get(source).send({ type: 'Input', props }),
60
88
  view: definition.view,
61
89
  });
62
90
  }
package/dist/tasks.d.ts CHANGED
@@ -1,18 +1,22 @@
1
+ import type { Snapshot } from './snapshot.js';
1
2
  import type { ModelOwner, TaskPolicy } from './owner.js';
2
3
  import { Effect } from 'effect';
3
4
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
4
5
  import type { View } from './dom.js';
5
- import { type Send, type RunningProgram } from './program.js';
6
+ import { type CommandSlot, type Send, type RunningProgram } from './program.js';
6
7
  import { type UiRuntime } from './runtime.js';
7
8
  export interface TaskDefinition<Model, Input, A, E, R = never> {
8
- readonly policy: 'drop' | 'replace';
9
- readonly run: (model: Model, input: Input) => Effect.Effect<A, E, R>;
9
+ readonly policy: Exclude<TaskPolicy, 'parallel'>;
10
+ readonly run: (model: Snapshot<Model>, input: Input) => Effect.Effect<A, E, R>;
10
11
  /** Domain identity, compared after fields or props change. Resets this slot only. */
11
- readonly identity?: (model: Model) => unknown;
12
+ readonly identity?: (model: Snapshot<Model>) => unknown;
12
13
  }
13
14
  type Definitions<Model, R> = Record<string, TaskDefinition<Model, never, unknown, unknown, R>>;
14
- type AnyDefinitions = Definitions<never, unknown>;
15
- type Input<T extends AnyDefinitions[string]> = Parameters<T['run']> extends [unknown, infer I, ...unknown[]] ? I : void;
15
+ type AnyDefinitions = Record<string, {
16
+ readonly run: (...args: never[]) => Effect.Effect<unknown, unknown, unknown>;
17
+ }>;
18
+ type Input<T extends AnyDefinitions[string]> = Parameters<T['run']> extends [unknown, ...infer Inputs] ? Inputs extends [] ? void : Inputs[0] : void;
19
+ type InputArguments<T extends AnyDefinitions[string]> = Input<T> extends void ? [input?: Input<T>] : Parameters<T['run']> extends [unknown, unknown, ...unknown[]] ? [input: Input<T>] : [input?: Input<T>];
16
20
  export type TaskResults<T extends AnyDefinitions> = {
17
21
  readonly [K in keyof T]: AsyncResult.AsyncResult<Effect.Success<ReturnType<T[K]['run']>>, Effect.Error<ReturnType<T[K]['run']>>>;
18
22
  };
@@ -22,7 +26,7 @@ export type TasksModel<Props, State, T extends AnyDefinitions> = State & {
22
26
  };
23
27
  export type TasksMessage<State, T extends AnyDefinitions> = {
24
28
  readonly type: 'Fields';
25
- readonly fields: Partial<State> & {
29
+ readonly fields: (Partial<State> | Partial<Snapshot<State>>) & {
26
30
  readonly props?: never;
27
31
  readonly tasks?: never;
28
32
  };
@@ -37,19 +41,19 @@ export type TasksMessage<State, T extends AnyDefinitions> = {
37
41
  };
38
42
  }[keyof T];
39
43
  type Init<Props, State> = {
40
- init: (props: Props) => State & {
44
+ init: (props: Snapshot<Props>) => (State | Snapshot<State>) & {
41
45
  readonly props?: never;
42
46
  readonly tasks?: never;
43
47
  };
44
48
  /** Changing the component entity also resets editable fields and all its tasks. */
45
- identity?: (props: Props) => unknown;
49
+ identity?: (props: Snapshot<Props>) => unknown;
46
50
  name?: string;
47
51
  };
48
52
  interface ControllerTask<R> {
49
53
  readonly run: (...args: never[]) => Effect.Effect<unknown, unknown, R>;
50
54
  readonly policy: TaskPolicy;
51
- /** Actions sharing a slot share cancellation and concurrency rules. Defaults to the action name. */
52
- readonly slot?: string;
55
+ /** Actions sharing a slot share cancellation and concurrency rules. Defaults to a fresh operation identity for this definition. */
56
+ readonly slot?: CommandSlot;
53
57
  }
54
58
  export declare function defineTasks<Owner extends Pick<ModelOwner<object>, 'run'>, T extends Record<string, ControllerTask<Effect.Services<Parameters<Owner['run']>[1]>>>>(owner: Owner, definitions: T): {
55
59
  readonly [K in keyof T]: (...args: Parameters<T[K]['run']>) => void;
@@ -62,11 +66,12 @@ declare function taskBuilder<Props, State extends object, R>(definition: Init<Pr
62
66
  tasks<T extends Definitions<State & {
63
67
  readonly props: Props;
64
68
  }, R>>(definitions: T): {
65
- create: (props: Props) => RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>;
66
- receive: (source: RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>, props: Props) => void;
69
+ slot: (name: keyof T) => CommandSlot;
70
+ create: (props: Props | Snapshot<Props>) => RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>;
71
+ receive: (source: RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>, props: Props | Snapshot<Props>) => void;
67
72
  controls: (send: Send<TasksMessage<State, T>>) => {
68
- run: <K extends keyof T>(task: K, ...input: Input<T[K]> extends void ? [input?: Input<T[K]>] : [input: Input<T[K]>]) => void;
69
- patch: (fields: Partial<State> & {
73
+ run: <K extends keyof T>(task: K, ...input: InputArguments<T[K]>) => void;
74
+ patch: (fields: (Partial<State> | Partial<Snapshot<State>>) & {
70
75
  readonly props?: never;
71
76
  readonly tasks?: never;
72
77
  }) => void;
package/dist/tasks.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { Effect, Option } from 'effect';
2
2
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
3
  import { programView } from './component.js';
4
- import { effectCommand, program } from './program.js';
4
+ import { commandSlot, effectCommand, program, } from './program.js';
5
5
  import { defaultUiRuntime } from './runtime.js';
6
6
  import { patchModel } from './state.js';
7
7
  export function defineTasks(definition, definitions) {
8
8
  if ('run' in definition) {
9
9
  const actions = Object.create(null);
10
10
  for (const [name, task] of Object.entries(definitions)) {
11
- actions[name] = (...args) => definition.run(task.slot ?? name, Effect.suspend(() => task.run(...args)), task.policy);
11
+ const slot = task.slot ?? commandSlot(name);
12
+ actions[name] = (...args) => definition.run(slot, Effect.suspend(() => task.run(...args)), task.policy);
12
13
  }
13
14
  return actions;
14
15
  }
@@ -27,19 +28,17 @@ function taskBuilder(definition, runtime) {
27
28
  return {
28
29
  tasks(definitions) {
29
30
  const names = Object.keys(definitions);
30
- const slot = (name) => `task:${String(name)}`;
31
+ const slots = new Map(names.map((name) => [name, commandSlot(`task:${name}`)]));
32
+ const slot = (name) => slots.get(name);
31
33
  const initialResults = () => Object.fromEntries(names.map((name) => [name, AsyncResult.initial()]));
32
- const init = (props) => ({
33
- ...definition.init(props),
34
- props,
35
- tasks: initialResults(),
36
- });
34
+ const init = (props) => ({ ...definition.init(props), props, tasks: initialResults() });
37
35
  const withResult = (model, task, result) => ({ ...model, tasks: { ...model.tasks, [task]: result } });
38
36
  const resetIdentities = (before, model) => {
39
37
  const cancel = [];
40
38
  for (const name of names) {
41
39
  const identity = definitions[name].identity;
42
- if (identity && !Object.is(identity(before), identity(model))) {
40
+ if (identity &&
41
+ !Object.is(identity(before), identity(model))) {
43
42
  model = withResult(model, name, AsyncResult.initial());
44
43
  cancel.push(slot(name));
45
44
  }
@@ -51,7 +50,9 @@ function taskBuilder(definition, runtime) {
51
50
  const source = program({
52
51
  initial: init(props),
53
52
  ...(definition.name ? { name: definition.name } : {}),
54
- update: (model, message) => {
53
+ update: (snapshot, message) => {
54
+ // Internal immutable reconstruction retains the declared domain types.
55
+ const model = snapshot;
55
56
  switch (message.type) {
56
57
  case 'Input':
57
58
  if (definition.identity &&
@@ -70,20 +71,22 @@ function taskBuilder(definition, runtime) {
70
71
  return {
71
72
  model: withResult(model, message.task, AsyncResult.waiting(previous)),
72
73
  commands: [
73
- effectCommand(slot(message.task), () => runtime.provide(task.run(model, message.input)), {
74
- onSuccess: (value) => ({
75
- type: 'Settled',
76
- task: message.task,
77
- result: AsyncResult.success(value),
78
- }),
79
- onFailure: (cause) => ({
80
- type: 'Settled',
81
- task: message.task,
82
- result: AsyncResult.failureWithPrevious(cause, {
83
- previous: Option.some(previous),
74
+ {
75
+ ...effectCommand(slot(message.task), () => runtime.provide(task.run(snapshot, message.input)), {
76
+ policy: task.policy,
77
+ onSuccess: (value) => ({
78
+ type: 'Settled',
79
+ task: message.task,
80
+ result: AsyncResult.success(value),
81
+ }),
82
+ onFailure: (cause) => ({
83
+ type: 'Settled',
84
+ task: message.task,
85
+ result: AsyncResult.failure(cause),
84
86
  }),
85
87
  }),
86
- }),
88
+ policy: task.policy,
89
+ },
87
90
  ],
88
91
  };
89
92
  }
@@ -97,8 +100,18 @@ function taskBuilder(definition, runtime) {
97
100
  model: withResult(model, message.task, AsyncResult.initial()),
98
101
  cancel: [slot(message.task)],
99
102
  };
100
- case 'Settled':
101
- return { model: withResult(model, message.task, message.result) };
103
+ case 'Settled': {
104
+ const result = AsyncResult.isFailure(message.result)
105
+ ? AsyncResult.failureWithPrevious(message.result.cause, {
106
+ previous: Option.some(model.tasks[message.task]),
107
+ })
108
+ : message.result;
109
+ return {
110
+ model: withResult(model, message.task, source.activeSlots().includes(slot(message.task))
111
+ ? AsyncResult.waiting(result)
112
+ : result),
113
+ };
114
+ }
102
115
  }
103
116
  },
104
117
  });
@@ -110,7 +123,7 @@ function taskBuilder(definition, runtime) {
110
123
  const owner = owners.get(source);
111
124
  if (!owner)
112
125
  throw new Error('Task source belongs to a different definition');
113
- owner.send({ type: 'Input', props });
126
+ owner.send({ type: 'Input', props: props });
114
127
  };
115
128
  const controls = (send) => ({
116
129
  run: (task, ...input) => send({ type: 'Run', task, input: input[0] }),
@@ -119,6 +132,7 @@ function taskBuilder(definition, runtime) {
119
132
  reset: (task) => send({ type: 'Reset', task }),
120
133
  });
121
134
  return {
135
+ slot,
122
136
  create,
123
137
  receive,
124
138
  controls,
package/dist/testing.d.ts CHANGED
@@ -1,19 +1,15 @@
1
1
  import { Effect } from 'effect';
2
- import type { RunningProgram } from './program.js';
2
+ import type { CommandSlot, Program, RunningProgram } from './program.js';
3
3
  import { type UiRuntime } from './runtime.js';
4
- /** Drive the real queue. Test services and TestClock effects use the same supplied context. */
5
- export declare function programDriver<M, Msg>(source: RunningProgram<M, Msg>): ReturnType<typeof makeDriver<M, Msg, never>>;
6
- export declare function programDriver<M, Msg, R>(source: RunningProgram<M, Msg>, runtime: UiRuntime<R>): ReturnType<typeof makeDriver<M, Msg, R>>;
7
- declare function makeDriver<M, Msg, R>(source: RunningProgram<M, Msg>, runtime: UiRuntime<R>): {
8
- model: () => Readonly<M>;
9
- send: import("./program.js").Send<Msg>;
10
- subscribe: (listener: (model: Readonly<M>) => void) => () => void;
11
- dispose: () => void;
12
- activeSlots: () => readonly string[];
13
- awaitSlot: (slot: string) => Promise<void>;
14
- awaitIdle: () => Promise<void>;
15
- run: <A, E>(effect: Effect.Effect<A, E, R>) => Promise<A>;
16
- };
4
+ /** Program inspection and controlled Effect execution with application-owned services. */
5
+ export interface ProgramDriver<M, Msg, R = never> extends Program<M, Msg> {
6
+ readonly activeSlots: RunningProgram<M, Msg>['activeSlots'];
7
+ readonly awaitSlot: (slot: CommandSlot) => Promise<void>;
8
+ readonly awaitIdle: () => Promise<void>;
9
+ readonly run: <A, E>(effect: Effect.Effect<A, E, R>) => Promise<A>;
10
+ }
11
+ export declare function programDriver<M, Msg>(source: RunningProgram<M, Msg>): ProgramDriver<M, Msg>;
12
+ export declare function programDriver<M, Msg, R>(source: RunningProgram<M, Msg>, runtime: UiRuntime<R>): ProgramDriver<M, Msg, R>;
17
13
  /** A reusable controlled request, with cancellation visible to tests. No renderer or private messages. */
18
14
  export declare function controlledEffect<A, E = never>(): {
19
15
  effect: Effect.Effect<A, E, never>;
@@ -23,4 +19,3 @@ export declare function controlledEffect<A, E = never>(): {
23
19
  fail: (error: E) => void;
24
20
  die: (defect: unknown) => void;
25
21
  };
26
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effectweb",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Immutable Effect models and compiled JSX with direct DOM rendering",
5
5
  "homepage": "https://github.com/DerpyCrabs/EffectWeb",
6
6
  "bugs": {
@@ -123,6 +123,10 @@
123
123
  "./testing": {
124
124
  "types": "./dist/testing.d.ts",
125
125
  "import": "./dist/testing.js"
126
+ },
127
+ "./http": {
128
+ "types": "./dist/http.d.ts",
129
+ "import": "./dist/http.js"
126
130
  }
127
131
  },
128
132
  "publishConfig": {