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.
- package/dist/AsyncContent.d.ts +1 -1
- package/dist/AsyncContent.js +18 -35
- package/dist/cache-internals.d.ts +3 -2
- package/dist/cache.d.ts +15 -12
- package/dist/cache.js +51 -10
- package/dist/collection.d.ts +1 -1
- package/dist/collection.js +1 -3
- package/dist/component.d.ts +1 -1
- package/dist/component.js +9 -4
- package/dist/dom.d.ts +64 -31
- package/dist/dom.js +237 -121
- package/dist/effectEvent.d.ts +10 -7
- package/dist/effectEvent.js +50 -46
- package/dist/form.d.ts +2 -1
- package/dist/http.d.ts +2 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +10 -8
- package/dist/infinite-query.d.ts +12 -8
- package/dist/infinite-query.js +21 -13
- package/dist/jsx-runtime.d.ts +6 -0
- package/dist/jsx-runtime.js +14 -1
- package/dist/jsx.d.ts +4 -1
- package/dist/keyed-tasks.d.ts +8 -6
- package/dist/keyed-tasks.js +29 -24
- package/dist/lazy.d.ts +4 -2
- package/dist/lazy.js +16 -10
- package/dist/load.d.ts +1 -1
- package/dist/load.js +1 -1
- package/dist/mount.d.ts +23 -8
- package/dist/mount.js +116 -27
- package/dist/owner.d.ts +13 -8
- package/dist/owner.js +37 -41
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +2 -1
- package/dist/program.d.ts +20 -8
- package/dist/program.js +44 -22
- package/dist/query-internals.d.ts +5 -3
- package/dist/query.d.ts +17 -6
- package/dist/query.js +6 -3
- package/dist/render.d.ts +17 -0
- package/dist/render.js +48 -0
- package/dist/resource.d.ts +4 -3
- package/dist/resource.js +12 -6
- package/dist/runtime.d.ts +12 -1
- package/dist/runtime.js +27 -5
- package/dist/session.d.ts +3 -2
- package/dist/session.js +15 -5
- package/dist/settlement.d.ts +8 -1
- package/dist/settlement.js +20 -8
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +10 -12
- package/dist/source.d.ts +38 -0
- package/dist/source.js +218 -0
- package/dist/task.d.ts +1 -1
- package/dist/task.js +3 -1
- package/dist/tasks.d.ts +9 -6
- package/dist/tasks.js +7 -4
- package/dist/testing.d.ts +5 -4
- package/dist/testing.js +4 -2
- package/package.json +10 -3
- package/README.md +0 -101
- package/dist/intrinsic.d.ts +0 -2
- package/dist/intrinsic.js +0 -120
- package/dist/slotIdentity.d.ts +0 -1
- package/dist/slotIdentity.js +0 -2
package/dist/mount.d.ts
CHANGED
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import type { Snapshot } from './snapshot.js';
|
|
2
2
|
import { type ReportError } from './errors.js';
|
|
3
|
-
import
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Scope from 'effect/Scope';
|
|
5
|
+
import * as Exit from 'effect/Exit';
|
|
4
6
|
import { type UiRuntime } from './runtime.js';
|
|
5
7
|
export { Portal, type PortalProps } from './dom.js';
|
|
6
8
|
type Work<R = never> = (() => void) | {
|
|
7
9
|
dispose: () => void;
|
|
8
10
|
update?: () => void;
|
|
9
|
-
} | Effect.Effect<
|
|
11
|
+
} | Effect.Effect<unknown, unknown, R | Scope.Scope>;
|
|
10
12
|
export interface DomMount<T extends Element = Element> {
|
|
11
13
|
readonly identity: unknown;
|
|
12
14
|
readonly data: unknown;
|
|
13
15
|
readonly acquire: (element: T, input: () => unknown) => Work;
|
|
14
16
|
}
|
|
15
17
|
/** Element-owned work. Changed acquisition functions replace and interrupt the prior lifetime. */
|
|
16
|
-
export declare function domMount<T extends Element, R = never>(start: (element: T) => Work<R>, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): DomMount<T>;
|
|
18
|
+
export declare function domMount<T extends Element, R = never>(start: (element: T) => Work<R>, ...provided: [Exclude<R, Scope.Scope>] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): DomMount<T>;
|
|
17
19
|
/** A stable DOM lifecycle with fresh immutable inputs, for focus, measurements and native events. */
|
|
18
|
-
export declare function domBinding<T extends Element, A, R = never>(data: A | Snapshot<A>, acquire: (element: T, input: () => Snapshot<A>) => Work<R>, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): DomMount<T>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export declare function domBinding<T extends Element, A, R = never>(data: A | Snapshot<A>, acquire: (element: T, input: () => Snapshot<A>) => Work<R>, ...provided: [Exclude<R, Scope.Scope>] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): DomMount<T>;
|
|
21
|
+
/** Describe a DOM acquisition in Effect setup; services are captured once, resources belong to the element. */
|
|
22
|
+
export declare const makeDomMount: <T extends Element, R = never>(start: (element: T) => Work<R>) => Effect.Effect<DomMount<T>, never, R>;
|
|
23
|
+
/** Capture services once and bind changing inputs without replacing the acquisition lifetime. */
|
|
24
|
+
export declare const makeDomBinding: <T extends Element, A, R = never>(acquire: (element: T, input: () => Snapshot<A>) => Work<R>) => Effect.Effect<(data: A | Snapshot<A>) => DomMount<T>, never, R>;
|
|
25
|
+
/** Allocate the lifetime before acquisition so reentrant publications can update or close it. */
|
|
26
|
+
export declare function prepareMount<T extends Element>(element: T, mount: DomMount<T>, report?: ReportError, runtime?: UiRuntime<never>): {
|
|
27
|
+
closed: Effect.Effect<void, never, never>;
|
|
28
|
+
start(): void;
|
|
29
|
+
close: () => Effect.Effect<void, never, never>;
|
|
22
30
|
update(next: DomMount<T>): boolean;
|
|
23
|
-
dispose: () => void;
|
|
31
|
+
dispose: (exit?: Exit.Exit<unknown, unknown>) => void;
|
|
32
|
+
};
|
|
33
|
+
export declare function startMount<T extends Element>(element: T, mount: DomMount<T>, report?: ReportError, runtime?: UiRuntime<never>): {
|
|
34
|
+
closed: Effect.Effect<void, never, never>;
|
|
35
|
+
start(): void;
|
|
36
|
+
close: () => Effect.Effect<void, never, never>;
|
|
37
|
+
update(next: DomMount<T>): boolean;
|
|
38
|
+
dispose: (exit?: Exit.Exit<unknown, unknown>) => void;
|
|
24
39
|
};
|
package/dist/mount.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { reportError, reportSafely } from './errors.js';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import * as Cause from 'effect/Cause';
|
|
3
|
+
import * as Deferred from 'effect/Deferred';
|
|
4
|
+
import * as Effect from 'effect/Effect';
|
|
5
|
+
import * as Fiber from 'effect/Fiber';
|
|
6
|
+
import * as Scope from 'effect/Scope';
|
|
7
|
+
import * as Exit from 'effect/Exit';
|
|
8
|
+
import { uiRuntime } from './runtime.js';
|
|
4
9
|
export { Portal } from './dom.js';
|
|
5
10
|
/** Element-owned work. Changed acquisition functions replace and interrupt the prior lifetime. */
|
|
6
11
|
export function domMount(start, ...provided) {
|
|
7
|
-
const runtime = provided[0]
|
|
12
|
+
const runtime = provided[0];
|
|
8
13
|
return {
|
|
9
14
|
identity: start,
|
|
10
15
|
data: undefined,
|
|
11
16
|
acquire: (element) => {
|
|
12
17
|
const work = start(element);
|
|
13
|
-
return Effect.isEffect(work) ? runtime.
|
|
18
|
+
return Effect.isEffect(work) && runtime ? runtime.provideScoped(work) : work;
|
|
14
19
|
},
|
|
15
20
|
};
|
|
16
21
|
}
|
|
@@ -21,31 +26,66 @@ export function domBinding(data, acquire, ...provided) {
|
|
|
21
26
|
data,
|
|
22
27
|
acquire: (element, input) => {
|
|
23
28
|
const work = acquire(element, input);
|
|
24
|
-
const runtime = provided[0]
|
|
25
|
-
return Effect.isEffect(work) ? runtime.
|
|
29
|
+
const runtime = provided[0];
|
|
30
|
+
return Effect.isEffect(work) && runtime ? runtime.provideScoped(work) : work;
|
|
26
31
|
},
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
|
-
|
|
34
|
+
/** Describe a DOM acquisition in Effect setup; services are captured once, resources belong to the element. */
|
|
35
|
+
export const makeDomMount = (start) => Effect.map(Effect.context(), (context) => {
|
|
36
|
+
const runtime = uiRuntime(context);
|
|
37
|
+
return {
|
|
38
|
+
identity: start,
|
|
39
|
+
data: undefined,
|
|
40
|
+
acquire: (element) => {
|
|
41
|
+
const work = start(element);
|
|
42
|
+
return Effect.isEffect(work) ? runtime.provideScoped(work) : work;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
/** Capture services once and bind changing inputs without replacing the acquisition lifetime. */
|
|
47
|
+
export const makeDomBinding = (acquire) => Effect.map(Effect.context(), (context) => {
|
|
48
|
+
const runtime = uiRuntime(context);
|
|
49
|
+
return (data) => ({
|
|
50
|
+
identity: acquire,
|
|
51
|
+
data,
|
|
52
|
+
acquire: (element, input) => {
|
|
53
|
+
const work = acquire(element, input);
|
|
54
|
+
return Effect.isEffect(work) ? runtime.provideScoped(work) : work;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
/** Allocate the lifetime before acquisition so reentrant publications can update or close it. */
|
|
59
|
+
export function prepareMount(element, mount, report = reportError, runtime) {
|
|
30
60
|
let data = mount.data;
|
|
31
|
-
|
|
32
|
-
|
|
61
|
+
let work;
|
|
62
|
+
let fiber;
|
|
63
|
+
const resourceScope = Scope.makeUnsafe();
|
|
64
|
+
let started = false;
|
|
33
65
|
let disposed = false;
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const dispose = () => {
|
|
44
|
-
if (disposed)
|
|
66
|
+
let released = false;
|
|
67
|
+
let closingExit;
|
|
68
|
+
let changedDuringSetup = false;
|
|
69
|
+
const completed = Deferred.makeUnsafe();
|
|
70
|
+
const closed = Deferred.await(completed);
|
|
71
|
+
const finish = () => Deferred.doneUnsafe(completed, Effect.void);
|
|
72
|
+
const release = (exit = Exit.void) => {
|
|
73
|
+
closingExit ??= exit;
|
|
74
|
+
if (released || work === undefined || (Effect.isEffect(work) && !fiber))
|
|
45
75
|
return;
|
|
46
|
-
|
|
47
|
-
if (fiber)
|
|
48
|
-
|
|
76
|
+
released = true;
|
|
77
|
+
if (fiber) {
|
|
78
|
+
// Acquisition may finish successfully while its resources remain mounted.
|
|
79
|
+
// Join acquisition before releasing its scope, including async finalizers.
|
|
80
|
+
Effect.runFork(Effect.gen(function* () {
|
|
81
|
+
yield* Fiber.interrupt(fiber);
|
|
82
|
+
yield* Scope.close(resourceScope, closingExit);
|
|
83
|
+
})).addObserver((exit) => {
|
|
84
|
+
if (exit._tag === 'Failure')
|
|
85
|
+
reportSafely(report, exit.cause);
|
|
86
|
+
finish();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
49
89
|
else {
|
|
50
90
|
try {
|
|
51
91
|
if (typeof work === 'function')
|
|
@@ -58,20 +98,69 @@ export function startMount(element, mount, report = reportError) {
|
|
|
58
98
|
}
|
|
59
99
|
}
|
|
60
100
|
};
|
|
101
|
+
const dispose = (exit = Exit.void) => {
|
|
102
|
+
if (disposed)
|
|
103
|
+
return;
|
|
104
|
+
disposed = true;
|
|
105
|
+
if (!started)
|
|
106
|
+
finish();
|
|
107
|
+
else
|
|
108
|
+
release(exit);
|
|
109
|
+
};
|
|
61
110
|
return {
|
|
62
111
|
closed,
|
|
63
|
-
|
|
112
|
+
start() {
|
|
113
|
+
if (started || disposed)
|
|
114
|
+
return;
|
|
115
|
+
started = true;
|
|
116
|
+
try {
|
|
117
|
+
work = mount.acquire(element, () => data);
|
|
118
|
+
if (Effect.isEffect(work)) {
|
|
119
|
+
const acquisition = Scope.provide(work, resourceScope);
|
|
120
|
+
fiber = runtime
|
|
121
|
+
? runtime.runFork(acquisition)
|
|
122
|
+
: Effect.runFork(Effect.scoped(acquisition));
|
|
123
|
+
fiber.addObserver((exit) => {
|
|
124
|
+
// User reporting may synchronously unmount; retain the acquisition failure first.
|
|
125
|
+
if (exit._tag === 'Failure')
|
|
126
|
+
closingExit ??= exit;
|
|
127
|
+
if (exit._tag === 'Failure' && (!disposed || !Cause.hasInterruptsOnly(exit.cause)))
|
|
128
|
+
reportSafely(report, exit.cause);
|
|
129
|
+
if (exit._tag === 'Failure')
|
|
130
|
+
release(exit);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (disposed)
|
|
134
|
+
release();
|
|
135
|
+
else if (changedDuringSetup && !fiber && typeof work === 'object' && 'update' in work)
|
|
136
|
+
work.update?.();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
dispose(Exit.die(error));
|
|
140
|
+
if (work === undefined)
|
|
141
|
+
finish();
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
close: () => Effect.suspend(() => {
|
|
64
146
|
dispose();
|
|
65
147
|
return closed;
|
|
66
|
-
},
|
|
148
|
+
}),
|
|
67
149
|
update(next) {
|
|
68
|
-
if (next.identity !== mount.identity)
|
|
150
|
+
if (disposed || next.identity !== mount.identity)
|
|
69
151
|
return false;
|
|
70
152
|
data = next.data;
|
|
71
|
-
if (
|
|
153
|
+
if (started && work === undefined)
|
|
154
|
+
changedDuringSetup = true;
|
|
155
|
+
else if (!fiber && typeof work === 'object' && work && 'update' in work)
|
|
72
156
|
work.update?.();
|
|
73
157
|
return true;
|
|
74
158
|
},
|
|
75
159
|
dispose,
|
|
76
160
|
};
|
|
77
161
|
}
|
|
162
|
+
export function startMount(element, mount, report = reportError, runtime) {
|
|
163
|
+
const lifetime = prepareMount(element, mount, report, runtime);
|
|
164
|
+
lifetime.start();
|
|
165
|
+
return lifetime;
|
|
166
|
+
}
|
package/dist/owner.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { type Snapshot } from './snapshot.js';
|
|
2
|
-
import
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import type * as Scope from 'effect/Scope';
|
|
3
4
|
import { type Program, type TaskPolicy, type CommandSlot } from './program.js';
|
|
4
5
|
import { type ReportError } from './errors.js';
|
|
5
|
-
import type
|
|
6
|
+
import { type UiRuntime } from './runtime.js';
|
|
6
7
|
export type { TaskPolicy } from './program.js';
|
|
8
|
+
interface OwnedResource {
|
|
9
|
+
dispose(): void;
|
|
10
|
+
close?(): Effect.Effect<void, unknown>;
|
|
11
|
+
}
|
|
7
12
|
export interface DisposableOwner {
|
|
8
13
|
readonly disposed: boolean;
|
|
9
|
-
readonly own: <A extends
|
|
10
|
-
dispose(): void;
|
|
11
|
-
}>(resource: A) => A;
|
|
14
|
+
readonly own: <A extends OwnedResource>(resource: A) => A;
|
|
12
15
|
}
|
|
13
16
|
export interface ModelOwner<Model extends object, R = never> extends DisposableOwner {
|
|
14
17
|
readonly source: Program<Model, never>;
|
|
@@ -21,12 +24,12 @@ export interface ModelOwner<Model extends object, R = never> extends DisposableO
|
|
|
21
24
|
readonly edit: <K extends keyof Model>(key: K, change: (value: Snapshot<Model[K]>) => Model[K] | Snapshot<Model[K]>) => void;
|
|
22
25
|
/** Synchronous transaction. Reads see staged changes; a throw discards changes and work. */
|
|
23
26
|
readonly transaction: <A>(work: () => A & (A extends PromiseLike<unknown> ? never : unknown)) => A;
|
|
24
|
-
readonly run: (slot: CommandSlot, effect: Effect.Effect<unknown, unknown, R>, policy: TaskPolicy) => void;
|
|
27
|
+
readonly run: (slot: CommandSlot, effect: Effect.Effect<unknown, unknown, R | Scope.Scope>, policy: TaskPolicy) => void;
|
|
25
28
|
readonly cancel: (slot: CommandSlot) => void;
|
|
26
29
|
readonly isRunning: (slot: CommandSlot) => boolean;
|
|
27
|
-
readonly awaitIdle: (slot?: CommandSlot) =>
|
|
30
|
+
readonly awaitIdle: (slot?: CommandSlot) => Effect.Effect<void>;
|
|
28
31
|
/** Interrupt and join work before closing owned dependencies in reverse order. */
|
|
29
|
-
readonly close: () =>
|
|
32
|
+
readonly close: () => Effect.Effect<void, AggregateError>;
|
|
30
33
|
readonly dispose: () => void;
|
|
31
34
|
}
|
|
32
35
|
type Options = {
|
|
@@ -37,3 +40,5 @@ export declare function modelOwner<Model extends object>(initial: Model, options
|
|
|
37
40
|
export declare function modelOwner<Model extends object, R>(initial: Model, options: Options & {
|
|
38
41
|
runtime: UiRuntime<R>;
|
|
39
42
|
}): ModelOwner<Model, R>;
|
|
43
|
+
/** Construct an owner using the current services and release it with the current Effect scope. */
|
|
44
|
+
export declare const makeModelOwner: <Model extends object, R = never>(initial: Model, options?: Options) => Effect.Effect<ModelOwner<Model, R>, never, R | Scope.Scope>;
|
package/dist/owner.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { protectSnapshot } from './snapshot.js';
|
|
2
|
-
import
|
|
2
|
+
import * as Cause from 'effect/Cause';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Exit from 'effect/Exit';
|
|
3
5
|
import { program, } from './program.js';
|
|
4
6
|
import { patchModel } from './state.js';
|
|
5
7
|
import { runAll, reportError } from './errors.js';
|
|
8
|
+
import { makeUiRuntime } from './runtime.js';
|
|
6
9
|
export function modelOwner(initial, options = {}) {
|
|
7
10
|
let disposed = false;
|
|
8
11
|
let staged;
|
|
9
12
|
const cleanups = [];
|
|
10
|
-
let
|
|
13
|
+
let resourcesDisposed = false;
|
|
11
14
|
const source = program({
|
|
12
15
|
initial,
|
|
13
|
-
...options,
|
|
16
|
+
...(options.name ? { name: options.name } : {}),
|
|
17
|
+
...(options.onDefect ? { onDefect: options.onDefect } : {}),
|
|
18
|
+
...(options.runtime
|
|
19
|
+
? {
|
|
20
|
+
runtime: {
|
|
21
|
+
runFork: (effect) => options.runtime.runFork(effect),
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
: {}),
|
|
14
25
|
update(snapshot, operations) {
|
|
15
26
|
let model = snapshot;
|
|
16
27
|
const single = operations.length === 1 ? operations[0] : undefined;
|
|
@@ -57,42 +68,27 @@ export function modelOwner(initial, options = {}) {
|
|
|
57
68
|
return;
|
|
58
69
|
disposed = true;
|
|
59
70
|
source.dispose();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.reverse()
|
|
63
|
-
.map((resource) => () => resource.dispose()), options.onDefect ?? reportError);
|
|
71
|
+
resourcesDisposed = true;
|
|
72
|
+
runAll([...cleanups].reverse().map((resource) => () => resource.dispose()), options.onDefect ?? reportError);
|
|
64
73
|
};
|
|
65
|
-
const
|
|
66
|
-
if (closing)
|
|
67
|
-
return closing;
|
|
74
|
+
const closing = Effect.runSync(Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
|
|
68
75
|
disposed = true;
|
|
69
76
|
const resources = cleanups.splice(0).reverse();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
errors.push(error);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (errors.length)
|
|
91
|
-
throw new AggregateError(errors, 'Owner cleanup failed.');
|
|
92
|
-
};
|
|
93
|
-
closeResources().then(finish, fail);
|
|
94
|
-
return closing;
|
|
95
|
-
};
|
|
77
|
+
yield* source.close();
|
|
78
|
+
const errors = [];
|
|
79
|
+
for (const resource of resources) {
|
|
80
|
+
const exit = yield* Effect.exit(Effect.suspend(() => resource.close
|
|
81
|
+
? resource.close()
|
|
82
|
+
: resourcesDisposed
|
|
83
|
+
? Effect.void
|
|
84
|
+
: Effect.sync(() => resource.dispose())));
|
|
85
|
+
if (Exit.isFailure(exit))
|
|
86
|
+
errors.push(Cause.squash(exit.cause));
|
|
87
|
+
}
|
|
88
|
+
if (errors.length)
|
|
89
|
+
return yield* Effect.fail(new AggregateError(errors, 'Owner cleanup failed.'));
|
|
90
|
+
}))));
|
|
91
|
+
const close = () => closing;
|
|
96
92
|
return {
|
|
97
93
|
source: { model: source.model, send: source.send, subscribe: source.subscribe, dispose, close },
|
|
98
94
|
read,
|
|
@@ -122,11 +118,6 @@ export function modelOwner(initial, options = {}) {
|
|
|
122
118
|
staged = batch;
|
|
123
119
|
try {
|
|
124
120
|
const result = work();
|
|
125
|
-
if (result &&
|
|
126
|
-
(typeof result === 'object' || typeof result === 'function') &&
|
|
127
|
-
'then' in result &&
|
|
128
|
-
typeof result.then === 'function')
|
|
129
|
-
throw new TypeError('Model transactions must be synchronous. Run async work as an owned Effect.');
|
|
130
121
|
staged = parent;
|
|
131
122
|
if (!disposed) {
|
|
132
123
|
if (parent) {
|
|
@@ -160,3 +151,8 @@ export function modelOwner(initial, options = {}) {
|
|
|
160
151
|
dispose,
|
|
161
152
|
};
|
|
162
153
|
}
|
|
154
|
+
/** Construct an owner using the current services and release it with the current Effect scope. */
|
|
155
|
+
export const makeModelOwner = (initial, options = {}) => Effect.gen(function* () {
|
|
156
|
+
const runtime = yield* makeUiRuntime();
|
|
157
|
+
return yield* Effect.acquireRelease(Effect.sync(() => modelOwner(initial, { ...options, runtime })), (owner) => owner.close().pipe(Effect.orDie));
|
|
158
|
+
});
|
package/dist/pages.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snapshot } from './snapshot.js';
|
|
2
|
-
import
|
|
2
|
+
import * as Cause from 'effect/Cause';
|
|
3
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
4
4
|
import type { UiLoad, UiPage } from './load.js';
|
|
5
5
|
import { type Transition, type RunningProgram } from './program.js';
|
package/dist/pages.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { commandSlot } from './program.js';
|
|
2
2
|
import { shareData } from './sharing.js';
|
|
3
|
-
import
|
|
3
|
+
import * as Cause from 'effect/Cause';
|
|
4
|
+
import * as Option from 'effect/Option';
|
|
4
5
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
5
6
|
import { effectCommand, program, } from './program.js';
|
|
6
7
|
import { defaultUiRuntime } from './runtime.js';
|
package/dist/program.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type Snapshot } from './snapshot.js';
|
|
2
|
-
import
|
|
2
|
+
import * as Cause from 'effect/Cause';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Stream from 'effect/Stream';
|
|
5
|
+
import type * as Scope from 'effect/Scope';
|
|
6
|
+
import { type UiRuntime } from './runtime.js';
|
|
3
7
|
export type Send<Message> = (message: Message) => void;
|
|
4
8
|
/** queue retains FIFO requests; latest-queued retains only the newest pending request. */
|
|
5
9
|
export type TaskPolicy = 'replace' | 'drop' | 'parallel' | 'queue' | 'latest-queued';
|
|
@@ -16,15 +20,15 @@ export type Command<Message, R = never> = {
|
|
|
16
20
|
/** Notification for work that never started. */
|
|
17
21
|
readonly onDiscard?: (reason: 'Cancelled' | 'Superseded' | 'Dropped') => void;
|
|
18
22
|
} & ({
|
|
19
|
-
readonly effect: Effect.Effect<Message, never, R>;
|
|
23
|
+
readonly effect: Effect.Effect<Message, never, R | Scope.Scope>;
|
|
20
24
|
readonly stream?: never;
|
|
21
25
|
readonly action?: never;
|
|
22
26
|
} | {
|
|
23
|
-
readonly stream: Stream.Stream<Message, never, R>;
|
|
27
|
+
readonly stream: Stream.Stream<Message, never, R | Scope.Scope>;
|
|
24
28
|
readonly effect?: never;
|
|
25
29
|
readonly action?: never;
|
|
26
30
|
} | {
|
|
27
|
-
readonly action: Effect.Effect<void, unknown, R>;
|
|
31
|
+
readonly action: Effect.Effect<void, unknown, R | Scope.Scope>;
|
|
28
32
|
readonly effect?: never;
|
|
29
33
|
readonly stream?: never;
|
|
30
34
|
});
|
|
@@ -51,19 +55,27 @@ export interface Program<Model, Message> {
|
|
|
51
55
|
readonly send: Send<Message>;
|
|
52
56
|
readonly subscribe: (listener: (model: Snapshot<Model>) => void) => () => void;
|
|
53
57
|
readonly dispose: () => void;
|
|
54
|
-
readonly close?: () =>
|
|
58
|
+
readonly close?: () => Effect.Effect<void, unknown>;
|
|
55
59
|
}
|
|
56
60
|
export interface RunningProgram<Model, Message> extends Program<Model, Message> {
|
|
57
61
|
/** Wait for interrupted work and its finalizers as well as admitted work. */
|
|
58
|
-
readonly awaitStopped: () =>
|
|
59
|
-
readonly close: () =>
|
|
62
|
+
readonly awaitStopped: () => Effect.Effect<void>;
|
|
63
|
+
readonly close: () => Effect.Effect<void>;
|
|
60
64
|
readonly activeSlots: () => readonly CommandSlot[];
|
|
61
|
-
readonly awaitIdle: (slot?: CommandSlot) =>
|
|
65
|
+
readonly awaitIdle: (slot?: CommandSlot) => Effect.Effect<void>;
|
|
62
66
|
}
|
|
63
67
|
export declare function program<Model, Message>(options: {
|
|
64
68
|
initial: Model | Snapshot<Model>;
|
|
65
69
|
name?: string;
|
|
66
70
|
update: (model: Snapshot<Model>, message: Message) => Transition<Model, Message>;
|
|
67
71
|
onDefect?: (cause: unknown) => void;
|
|
72
|
+
runtime?: Pick<UiRuntime<never>, 'runFork'>;
|
|
68
73
|
}): RunningProgram<Model, Message>;
|
|
74
|
+
/** Construct a program in the current Effect environment and own it in the current scope. */
|
|
75
|
+
export declare const makeProgram: <Model, Message, R = never>(options: {
|
|
76
|
+
initial: Model | Snapshot<Model>;
|
|
77
|
+
name?: string;
|
|
78
|
+
update: (model: Snapshot<Model>, message: Message) => Transition<Model, Message, R>;
|
|
79
|
+
onDefect?: (cause: unknown) => void;
|
|
80
|
+
}) => Effect.Effect<RunningProgram<Model, Message>, never, R | Scope.Scope>;
|
|
69
81
|
export {};
|
package/dist/program.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { protectSnapshot } from './snapshot.js';
|
|
2
2
|
import { reportError, reportSafely } from './errors.js';
|
|
3
3
|
import { traceProgram, nextProgramId, hasProgramObservers } from './diagnostics.js';
|
|
4
|
-
import
|
|
4
|
+
import * as Cause from 'effect/Cause';
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import * as Fiber from 'effect/Fiber';
|
|
7
|
+
import * as Option from 'effect/Option';
|
|
8
|
+
import * as Stream from 'effect/Stream';
|
|
9
|
+
import { makeUiRuntime } from './runtime.js';
|
|
5
10
|
export const commandSlot = (name) => Symbol(name);
|
|
6
|
-
const taskPolicies = ['replace', 'drop', 'parallel', 'queue', 'latest-queued'];
|
|
7
11
|
/** A lazy request settles successes, typed failures and defects through one message contract. */
|
|
8
12
|
export function effectCommand(slot, load, handlers) {
|
|
9
13
|
return {
|
|
10
14
|
slot,
|
|
11
15
|
policy: handlers.policy,
|
|
12
|
-
|
|
16
|
+
// Release the operation with its actual exit before translating it into a message.
|
|
17
|
+
effect: Effect.scoped(Effect.suspend(load)).pipe(Effect.matchCause(handlers)),
|
|
13
18
|
};
|
|
14
19
|
}
|
|
15
20
|
/** Owned work with no model result. Failures use the program's error reporter. */
|
|
@@ -68,13 +73,20 @@ export function program(options) {
|
|
|
68
73
|
const notifyStopped = () => {
|
|
69
74
|
if (live.size || running.size || draining)
|
|
70
75
|
return;
|
|
71
|
-
|
|
72
|
-
done();
|
|
76
|
+
const completed = [...stopped];
|
|
73
77
|
stopped.clear();
|
|
78
|
+
for (const done of completed)
|
|
79
|
+
done();
|
|
74
80
|
};
|
|
75
|
-
const awaitStopped = () =>
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const awaitStopped = () => Effect.callback((resume) => {
|
|
82
|
+
if (live.size === 0 && running.size === 0 && !draining)
|
|
83
|
+
return resume(Effect.void);
|
|
84
|
+
const done = () => resume(Effect.void);
|
|
85
|
+
stopped.add(done);
|
|
86
|
+
return Effect.sync(() => {
|
|
87
|
+
stopped.delete(done);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
78
90
|
let disposed = false;
|
|
79
91
|
let draining = false;
|
|
80
92
|
const queue = [];
|
|
@@ -142,13 +154,18 @@ export function program(options) {
|
|
|
142
154
|
? command.action.pipe(Effect.as(Option.none()))
|
|
143
155
|
: command.effect.pipe(Effect.map(Option.some));
|
|
144
156
|
live.add(task);
|
|
145
|
-
const fiber =
|
|
157
|
+
const fiber = options.runtime
|
|
158
|
+
? options.runtime.runFork(effect)
|
|
159
|
+
: Effect.runFork(Effect.scoped(effect));
|
|
146
160
|
task.fiber = fiber;
|
|
147
161
|
fiber.addObserver((exit) => {
|
|
148
162
|
live.delete(task);
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
if (!valid()) {
|
|
164
|
+
if (exit._tag === 'Failure' && !Cause.hasInterruptsOnly(exit.cause))
|
|
165
|
+
reportSafely(options.onDefect ?? reportError, exit.cause);
|
|
166
|
+
notifyStopped();
|
|
151
167
|
return;
|
|
168
|
+
}
|
|
152
169
|
group.active.delete(task);
|
|
153
170
|
if (!group.active.size && !group.pending.length)
|
|
154
171
|
running.delete(command.slot);
|
|
@@ -198,10 +215,6 @@ export function program(options) {
|
|
|
198
215
|
}
|
|
199
216
|
const { message } = queue.shift();
|
|
200
217
|
const transition = options.update(current, message);
|
|
201
|
-
for (const command of transition.commands ?? []) {
|
|
202
|
-
if (typeof command.slot !== 'symbol' || !taskPolicies.includes(command.policy))
|
|
203
|
-
throw new TypeError('Commands require a commandSlot() token and an explicit concurrency policy.');
|
|
204
|
-
}
|
|
205
218
|
trace('update', undefined, message);
|
|
206
219
|
for (const slot of transition.cancel ?? [])
|
|
207
220
|
cancel(slot);
|
|
@@ -276,17 +289,21 @@ export function program(options) {
|
|
|
276
289
|
};
|
|
277
290
|
return {
|
|
278
291
|
awaitStopped,
|
|
279
|
-
close: () => {
|
|
292
|
+
close: () => Effect.suspend(() => {
|
|
280
293
|
dispose();
|
|
281
294
|
return awaitStopped();
|
|
282
|
-
},
|
|
295
|
+
}),
|
|
283
296
|
model: () => current,
|
|
284
297
|
activeSlots: () => [...running.keys()],
|
|
285
|
-
awaitIdle: (slot) =>
|
|
286
|
-
?
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
awaitIdle: (slot) => Effect.callback((resume) => {
|
|
299
|
+
if (disposed || (!draining && (slot ? !running.has(slot) : running.size === 0)))
|
|
300
|
+
return resume(Effect.void);
|
|
301
|
+
const waiter = { slot, done: () => resume(Effect.void) };
|
|
302
|
+
waiters.add(waiter);
|
|
303
|
+
return Effect.sync(() => {
|
|
304
|
+
waiters.delete(waiter);
|
|
305
|
+
});
|
|
306
|
+
}),
|
|
290
307
|
send,
|
|
291
308
|
subscribe(listener) {
|
|
292
309
|
if (disposed)
|
|
@@ -307,3 +324,8 @@ export function program(options) {
|
|
|
307
324
|
dispose,
|
|
308
325
|
};
|
|
309
326
|
}
|
|
327
|
+
/** Construct a program in the current Effect environment and own it in the current scope. */
|
|
328
|
+
export const makeProgram = (options) => Effect.gen(function* () {
|
|
329
|
+
const runtime = yield* makeUiRuntime();
|
|
330
|
+
return yield* Effect.acquireRelease(Effect.sync(() => runtime.program(options)), (source) => source.close());
|
|
331
|
+
});
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type * as Effect from 'effect/Effect';
|
|
2
|
+
import type * as Scope from 'effect/Scope';
|
|
2
3
|
import type { Snapshot } from './snapshot.js';
|
|
3
|
-
import type { Query, QueryGroup } from './query.js';
|
|
4
|
+
import type { Query, QueryGroup, QueryKey } from './query.js';
|
|
4
5
|
export interface QueryDefinition<Args, A, E, R> {
|
|
5
6
|
readonly name: string;
|
|
6
|
-
readonly load: (args: Snapshot<Args>, previous?: Snapshot<A>) => Effect.Effect<A, E, R>;
|
|
7
|
+
readonly load: (args: Snapshot<Args>, previous?: Snapshot<A>) => Effect.Effect<A, E, R | Scope.Scope>;
|
|
7
8
|
readonly groups?: readonly QueryGroup[];
|
|
8
9
|
readonly unused?: 'retain' | 'cancel';
|
|
9
10
|
readonly staleTime: number;
|
|
10
11
|
readonly share?: (previous: Snapshot<A>, next: A | Snapshot<A>) => A | Snapshot<A>;
|
|
12
|
+
readonly encode?: (args: Snapshot<Args>) => QueryKey;
|
|
11
13
|
}
|
|
12
14
|
export declare function registerQuery<Args, A, E, R>(query: Query<Args, A, E, R>, definition: QueryDefinition<Args, A, E, R>): void;
|
|
13
15
|
export declare function queryDefinition<Args, A, E, R>(query: Query<Args, A, E, R>): QueryDefinition<Args, A, E, R>;
|