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,69 @@
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, type RunningProgram } from './program.js';
5
+ import { type UiRuntime } from './runtime.js';
6
+ export interface TaskDefinition<Model, Input, A, E, R = never> {
7
+ readonly policy: 'drop' | 'replace';
8
+ readonly run: (model: Model, input: Input) => Effect.Effect<A, E, R>;
9
+ /** Domain identity, compared after fields or props change. Resets this slot only. */
10
+ readonly identity?: (model: Model) => unknown;
11
+ }
12
+ type Definitions<Model, R> = Record<string, TaskDefinition<Model, never, unknown, unknown, R>>;
13
+ type AnyDefinitions = Definitions<never, unknown>;
14
+ type Input<T extends AnyDefinitions[string]> = Parameters<T['run']> extends [unknown, infer I, ...unknown[]] ? I : void;
15
+ export type TaskResults<T extends AnyDefinitions> = {
16
+ readonly [K in keyof T]: AsyncResult.AsyncResult<Effect.Success<ReturnType<T[K]['run']>>, Effect.Error<ReturnType<T[K]['run']>>>;
17
+ };
18
+ export type TasksModel<Props, State, T extends AnyDefinitions> = State & {
19
+ readonly props: Props;
20
+ readonly tasks: TaskResults<T>;
21
+ };
22
+ export type TasksMessage<State, T extends AnyDefinitions> = {
23
+ readonly type: 'Fields';
24
+ readonly fields: Partial<State> & {
25
+ readonly props?: never;
26
+ readonly tasks?: never;
27
+ };
28
+ } | {
29
+ readonly type: 'Cancel' | 'Reset';
30
+ readonly task: keyof T;
31
+ } | {
32
+ [K in keyof T]: {
33
+ readonly type: 'Run';
34
+ readonly task: K;
35
+ readonly input: Input<T[K]>;
36
+ };
37
+ }[keyof T];
38
+ type Init<Props, State> = {
39
+ init: (props: Props) => State & {
40
+ readonly props?: never;
41
+ readonly tasks?: never;
42
+ };
43
+ /** Changing the component entity also resets editable fields and all its tasks. */
44
+ identity?: (props: Props) => unknown;
45
+ name?: string;
46
+ };
47
+ export declare function defineTasks<Props, State extends object, R>(definition: Init<Props, State> & {
48
+ runtime: UiRuntime<R>;
49
+ }): ReturnType<typeof taskBuilder<Props, State, R>>;
50
+ export declare function defineTasks<Props, State extends object>(definition: Init<Props, State>): ReturnType<typeof taskBuilder<Props, State, never>>;
51
+ declare function taskBuilder<Props, State extends object, R>(definition: Init<Props, State>, runtime: UiRuntime<R>): {
52
+ tasks<T extends Definitions<State & {
53
+ readonly props: Props;
54
+ }, R>>(definitions: T): {
55
+ create: (props: Props) => RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>;
56
+ receive: (source: RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>, props: Props) => void;
57
+ controls: (send: Send<TasksMessage<State, T>>) => {
58
+ run: <K extends keyof T>(task: K, ...input: Input<T[K]> extends void ? [input?: Input<T[K]>] : [input: Input<T[K]>]) => void;
59
+ patch: (fields: Partial<State> & {
60
+ readonly props?: never;
61
+ readonly tasks?: never;
62
+ }) => void;
63
+ cancel: (task: keyof T) => void;
64
+ reset: (task: keyof T) => void;
65
+ };
66
+ view: (view: View<TasksModel<Props, State, T>, TasksMessage<State, T>>) => View<Props, never>;
67
+ };
68
+ };
69
+ export {};
package/dist/tasks.js ADDED
@@ -0,0 +1,126 @@
1
+ import { Effect, Option } from 'effect';
2
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
+ import { programView } from './component.js';
4
+ import { effectCommand, program } from './program.js';
5
+ import { defaultUiRuntime } from './runtime.js';
6
+ import { patchModel } from './state.js';
7
+ export function defineTasks(definition) {
8
+ return taskBuilder(definition, definition.runtime ?? defaultUiRuntime);
9
+ }
10
+ function stopWaiting(result) {
11
+ if (!result.waiting)
12
+ return result;
13
+ if (AsyncResult.isInitial(result))
14
+ return AsyncResult.initial();
15
+ if (AsyncResult.isSuccess(result))
16
+ return AsyncResult.success(result.value);
17
+ return AsyncResult.failureWithPrevious(result.cause, { previous: Option.some(result) });
18
+ }
19
+ function taskBuilder(definition, runtime) {
20
+ return {
21
+ tasks(definitions) {
22
+ const names = Object.keys(definitions);
23
+ const slot = (name) => `task:${String(name)}`;
24
+ const initialResults = () => Object.fromEntries(names.map((name) => [name, AsyncResult.initial()]));
25
+ const init = (props) => ({
26
+ ...definition.init(props),
27
+ props,
28
+ tasks: initialResults(),
29
+ });
30
+ const withResult = (model, task, result) => ({ ...model, tasks: { ...model.tasks, [task]: result } });
31
+ const resetIdentities = (before, model) => {
32
+ const cancel = [];
33
+ for (const name of names) {
34
+ const identity = definitions[name].identity;
35
+ if (identity && !Object.is(identity(before), identity(model))) {
36
+ model = withResult(model, name, AsyncResult.initial());
37
+ cancel.push(slot(name));
38
+ }
39
+ }
40
+ return { model, cancel };
41
+ };
42
+ const owners = new WeakMap();
43
+ const create = (props) => {
44
+ const source = program({
45
+ initial: init(props),
46
+ ...(definition.name ? { name: definition.name } : {}),
47
+ update: (model, message) => {
48
+ switch (message.type) {
49
+ case 'Input':
50
+ if (definition.identity &&
51
+ !Object.is(definition.identity(model.props), definition.identity(message.props)))
52
+ return { model: init(message.props), cancel: names.map(slot) };
53
+ return resetIdentities(model, { ...model, props: message.props });
54
+ case 'Fields': {
55
+ const next = patchModel(model, message.fields);
56
+ return resetIdentities(model, next === model ? model : { ...next, props: model.props, tasks: model.tasks });
57
+ }
58
+ case 'Run': {
59
+ const task = definitions[message.task];
60
+ const previous = model.tasks[message.task];
61
+ if (task.policy === 'drop' && previous.waiting)
62
+ return { model };
63
+ return {
64
+ model: withResult(model, message.task, AsyncResult.waiting(previous)),
65
+ commands: [
66
+ effectCommand(slot(message.task), () => runtime.provide(task.run(model, message.input)), {
67
+ onSuccess: (value) => ({
68
+ type: 'Settled',
69
+ task: message.task,
70
+ result: AsyncResult.success(value),
71
+ }),
72
+ onFailure: (cause) => ({
73
+ type: 'Settled',
74
+ task: message.task,
75
+ result: AsyncResult.failureWithPrevious(cause, {
76
+ previous: Option.some(previous),
77
+ }),
78
+ }),
79
+ }),
80
+ ],
81
+ };
82
+ }
83
+ case 'Cancel':
84
+ return {
85
+ model: withResult(model, message.task, stopWaiting(model.tasks[message.task])),
86
+ cancel: [slot(message.task)],
87
+ };
88
+ case 'Reset':
89
+ return {
90
+ model: withResult(model, message.task, AsyncResult.initial()),
91
+ cancel: [slot(message.task)],
92
+ };
93
+ case 'Settled':
94
+ return { model: withResult(model, message.task, message.result) };
95
+ }
96
+ },
97
+ });
98
+ const exposed = { ...source, send: source.send };
99
+ owners.set(exposed, source);
100
+ return exposed;
101
+ };
102
+ const receive = (source, props) => {
103
+ const owner = owners.get(source);
104
+ if (!owner)
105
+ throw new Error('Task source belongs to a different definition');
106
+ owner.send({ type: 'Input', props });
107
+ };
108
+ const controls = (send) => ({
109
+ run: (task, ...input) => send({ type: 'Run', task, input: input[0] }),
110
+ patch: (fields) => send({ type: 'Fields', fields }),
111
+ cancel: (task) => send({ type: 'Cancel', task }),
112
+ reset: (task) => send({ type: 'Reset', task }),
113
+ });
114
+ return {
115
+ create,
116
+ receive,
117
+ controls,
118
+ view: (view) => programView({
119
+ create,
120
+ receive: (source, props) => receive(source, props),
121
+ view,
122
+ }),
123
+ };
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,26 @@
1
+ import { Effect } from 'effect';
2
+ import type { RunningProgram } from './program.js';
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: () => M;
9
+ send: import("./program.js").Send<Msg>;
10
+ subscribe: (listener: (model: 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
+ };
17
+ /** A reusable controlled request, with cancellation visible to tests. No renderer or private messages. */
18
+ export declare function controlledEffect<A, E = never>(): {
19
+ effect: Effect.Effect<A, E, never>;
20
+ pending: () => number;
21
+ canceled: () => number;
22
+ succeed: (value: A) => void;
23
+ fail: (error: E) => void;
24
+ die: (defect: unknown) => void;
25
+ };
26
+ export {};
@@ -0,0 +1,44 @@
1
+ import { Cause, Effect } from 'effect';
2
+ import { defaultUiRuntime } from './runtime.js';
3
+ export function programDriver(source, runtime) {
4
+ return makeDriver(source, runtime ?? defaultUiRuntime);
5
+ }
6
+ function makeDriver(source, runtime) {
7
+ return {
8
+ model: source.model,
9
+ send: source.send,
10
+ subscribe: source.subscribe,
11
+ dispose: source.dispose,
12
+ activeSlots: source.activeSlots,
13
+ awaitSlot: (slot) => source.awaitIdle(slot),
14
+ awaitIdle: () => source.awaitIdle(),
15
+ run: (effect) => Effect.runPromise(runtime.provide(effect)),
16
+ };
17
+ }
18
+ /** A reusable controlled request, with cancellation visible to tests. No renderer or private messages. */
19
+ export function controlledEffect() {
20
+ const pending = new Set();
21
+ let canceled = 0;
22
+ const effect = Effect.callback((resume) => {
23
+ pending.add(resume);
24
+ return Effect.sync(() => {
25
+ pending.delete(resume);
26
+ canceled++;
27
+ });
28
+ });
29
+ const settle = (result) => {
30
+ const resume = pending.values().next().value;
31
+ if (!resume)
32
+ throw new Error('No controlled request is pending');
33
+ pending.delete(resume);
34
+ resume(result);
35
+ };
36
+ return {
37
+ effect,
38
+ pending: () => pending.size,
39
+ canceled: () => canceled,
40
+ succeed: (value) => settle(Effect.succeed(value)),
41
+ fail: (error) => settle(Effect.fail(error)),
42
+ die: (defect) => settle(Effect.failCause(Cause.die(defect))),
43
+ };
44
+ }
package/package.json ADDED
@@ -0,0 +1,137 @@
1
+ {
2
+ "name": "effectweb",
3
+ "version": "0.1.0",
4
+ "description": "Immutable Effect models and compiled JSX with direct DOM rendering",
5
+ "homepage": "https://github.com/DerpyCrabs/EffectWeb",
6
+ "bugs": {
7
+ "url": "https://github.com/DerpyCrabs/EffectWeb/issues"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/DerpyCrabs/EffectWeb.git",
13
+ "directory": "packages/runtime"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./jsx-runtime": {
28
+ "types": "./dist/jsx-runtime.d.ts",
29
+ "import": "./dist/jsx-runtime.js"
30
+ },
31
+ "./jsx-dev-runtime": {
32
+ "types": "./dist/jsx-runtime.d.ts",
33
+ "import": "./dist/jsx-runtime.js"
34
+ },
35
+ "./collection": {
36
+ "types": "./dist/collection.d.ts",
37
+ "import": "./dist/collection.js"
38
+ },
39
+ "./program": {
40
+ "types": "./dist/program.d.ts",
41
+ "import": "./dist/program.js"
42
+ },
43
+ "./dom": {
44
+ "types": "./dist/dom.d.ts",
45
+ "import": "./dist/dom.js"
46
+ },
47
+ "./jsx": {
48
+ "types": "./dist/jsx.d.ts",
49
+ "import": "./dist/jsx.js"
50
+ },
51
+ "./component": {
52
+ "types": "./dist/component.d.ts",
53
+ "import": "./dist/component.js"
54
+ },
55
+ "./mount": {
56
+ "types": "./dist/mount.d.ts",
57
+ "import": "./dist/mount.js"
58
+ },
59
+ "./pages": {
60
+ "types": "./dist/pages.d.ts",
61
+ "import": "./dist/pages.js"
62
+ },
63
+ "./share": {
64
+ "types": "./dist/share.d.ts",
65
+ "import": "./dist/share.js"
66
+ },
67
+ "./load": {
68
+ "types": "./dist/load.d.ts",
69
+ "import": "./dist/load.js"
70
+ },
71
+ "./errors": {
72
+ "types": "./dist/errors.d.ts",
73
+ "import": "./dist/errors.js"
74
+ },
75
+ "./diagnostics": {
76
+ "types": "./dist/diagnostics.d.ts",
77
+ "import": "./dist/diagnostics.js"
78
+ },
79
+ "./state": {
80
+ "types": "./dist/state.d.ts",
81
+ "import": "./dist/state.js"
82
+ },
83
+ "./actions": {
84
+ "types": "./dist/actions.d.ts",
85
+ "import": "./dist/actions.js"
86
+ },
87
+ "./task": {
88
+ "types": "./dist/task.d.ts",
89
+ "import": "./dist/task.js"
90
+ },
91
+ "./effectEvent": {
92
+ "types": "./dist/effectEvent.d.ts",
93
+ "import": "./dist/effectEvent.js"
94
+ },
95
+ "./runtime": {
96
+ "types": "./dist/runtime.d.ts",
97
+ "import": "./dist/runtime.js"
98
+ },
99
+ "./form": {
100
+ "types": "./dist/form.d.ts",
101
+ "import": "./dist/form.js"
102
+ },
103
+ "./query": {
104
+ "types": "./dist/query.d.ts",
105
+ "import": "./dist/query.js"
106
+ },
107
+ "./tasks": {
108
+ "types": "./dist/tasks.d.ts",
109
+ "import": "./dist/tasks.js"
110
+ },
111
+ "./resource": {
112
+ "types": "./dist/resource.d.ts",
113
+ "import": "./dist/resource.js"
114
+ },
115
+ "./cache": {
116
+ "types": "./dist/cache.d.ts",
117
+ "import": "./dist/cache.js"
118
+ },
119
+ "./session": {
120
+ "types": "./dist/session.d.ts",
121
+ "import": "./dist/session.js"
122
+ },
123
+ "./testing": {
124
+ "types": "./dist/testing.d.ts",
125
+ "import": "./dist/testing.js"
126
+ }
127
+ },
128
+ "publishConfig": {
129
+ "access": "public"
130
+ },
131
+ "peerDependencies": {
132
+ "effect": "4.0.0-rc.112"
133
+ },
134
+ "engines": {
135
+ "node": ">=22.14"
136
+ }
137
+ }