effectweb 0.4.0 → 0.5.1
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/cache-internals.d.ts +3 -2
- package/dist/cache.d.ts +13 -10
- package/dist/cache.js +44 -7
- package/dist/component.d.ts +1 -1
- package/dist/component.js +4 -2
- package/dist/dom.d.ts +39 -9
- package/dist/dom.js +93 -6
- package/dist/effectEvent.d.ts +8 -5
- package/dist/effectEvent.js +35 -19
- package/dist/index.d.ts +10 -8
- package/dist/index.js +10 -8
- package/dist/infinite-query.d.ts +14 -7
- package/dist/infinite-query.js +24 -12
- package/dist/jsx.d.ts +3 -1
- package/dist/keyed-tasks.d.ts +3 -2
- package/dist/keyed-tasks.js +2 -2
- package/dist/lazy.d.ts +2 -1
- package/dist/lazy.js +3 -2
- package/dist/mount.d.ts +13 -7
- package/dist/mount.js +62 -16
- package/dist/owner.d.ts +5 -2
- package/dist/owner.js +15 -1
- package/dist/program.d.ts +13 -3
- package/dist/program.js +11 -2
- package/dist/query-internals.d.ts +4 -2
- package/dist/query.d.ts +16 -5
- package/dist/query.js +6 -1
- package/dist/render.d.ts +17 -0
- package/dist/render.js +48 -0
- package/dist/resource.d.ts +4 -3
- package/dist/resource.js +9 -5
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +24 -4
- package/dist/session.d.ts +3 -2
- package/dist/session.js +5 -2
- package/dist/settlement.d.ts +6 -0
- package/dist/settlement.js +7 -0
- package/dist/source.d.ts +38 -0
- package/dist/source.js +218 -0
- package/dist/tasks.d.ts +8 -5
- package/dist/tasks.js +5 -3
- package/package.json +10 -3
- package/README.md +0 -132
package/dist/query.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type * as Effect from 'effect/Effect';
|
|
2
|
+
import type * as Scope from 'effect/Scope';
|
|
2
3
|
import { type QueryDefinition } from './query-internals.js';
|
|
4
|
+
import type { Snapshot } from './snapshot.js';
|
|
3
5
|
/** Canonical data identity: plain objects, dense arrays, and finite scalar values. */
|
|
4
6
|
export type QueryKey = string | number | boolean | null | undefined | readonly QueryKey[] | {
|
|
5
7
|
readonly [key: string]: QueryKey;
|
|
@@ -14,18 +16,27 @@ declare const queryType: unique symbol;
|
|
|
14
16
|
/** An opaque typed definition. Every request argument participates in cache identity. */
|
|
15
17
|
export interface Query<Args, A, E = never, R = never> {
|
|
16
18
|
readonly name: string;
|
|
17
|
-
readonly [queryType]: (args: Args) => Effect.Effect<A, E, R>;
|
|
19
|
+
readonly [queryType]: (args: Args) => Effect.Effect<A, E, R | Scope.Scope>;
|
|
18
20
|
}
|
|
19
21
|
type Definition<Args, A, E, R> = Omit<QueryDefinition<Args, A, E, R>, 'staleTime'> & {
|
|
20
22
|
readonly staleTime?: number;
|
|
21
23
|
/** Custom projections can silently reuse a different request's result. */
|
|
22
24
|
readonly key?: never;
|
|
23
25
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/** Encode the complete argument value explicitly when its representation is not plain data. */
|
|
27
|
+
export type QueryEncoding<Args> = [Args] extends [QueryArgs<Args>] ? {
|
|
28
|
+
readonly encode?: (args: Snapshot<Args>) => QueryKey;
|
|
29
|
+
} : {
|
|
30
|
+
readonly encode: (args: Snapshot<Args>) => QueryKey;
|
|
31
|
+
};
|
|
32
|
+
/** Shared identity contract for lookup, selection, writes, invalidation and pagination. */
|
|
33
|
+
export declare function encodeQueryArguments<Args, A, E, R>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): string;
|
|
34
|
+
export declare function query<Args, A, E = never, R = never>(definition: Definition<Args, A, E, R> & {
|
|
35
|
+
readonly encode: (args: Snapshot<Args>) => QueryKey;
|
|
36
|
+
}): Query<Args, A, E, R>;
|
|
37
|
+
export declare function query<Args, A, E = never, R = never>(definition: Definition<Args, A, E, R> & QueryEncoding<Args>): Query<Args, A, E, R>;
|
|
27
38
|
export declare function query<A, E = never, R = never>(definition: Definition<true, A, E, R> & {
|
|
28
|
-
readonly load: () => Effect.Effect<A, E, R>;
|
|
39
|
+
readonly load: () => Effect.Effect<A, E, R | Scope.Scope>;
|
|
29
40
|
}): Query<true, A, E, R>;
|
|
30
41
|
declare const groupType: unique symbol;
|
|
31
42
|
export type QueryGroup = symbol & {
|
package/dist/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { registerQuery } from './query-internals.js';
|
|
1
|
+
import { registerQuery, queryDefinition } from './query-internals.js';
|
|
2
2
|
/** Shared by cache lookup and resource reconciliation. Object property order is irrelevant. */
|
|
3
3
|
export function encodeQueryKey(key) {
|
|
4
4
|
const ancestors = new Set();
|
|
@@ -53,6 +53,11 @@ export function encodeQueryKey(key) {
|
|
|
53
53
|
};
|
|
54
54
|
return encode(key);
|
|
55
55
|
}
|
|
56
|
+
/** Shared identity contract for lookup, selection, writes, invalidation and pagination. */
|
|
57
|
+
export function encodeQueryArguments(definition, args) {
|
|
58
|
+
const config = queryDefinition(definition);
|
|
59
|
+
return encodeQueryKey(config.encode ? config.encode(args) : args);
|
|
60
|
+
}
|
|
56
61
|
export function query(definition) {
|
|
57
62
|
const staleTime = definition.staleTime ?? Infinity;
|
|
58
63
|
if (Number.isNaN(staleTime) || staleTime < 0)
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import * as Scope from 'effect/Scope';
|
|
3
|
+
import { type Mounted, type View } from './dom.js';
|
|
4
|
+
import type { Source } from './source.js';
|
|
5
|
+
import type { Send } from './program.js';
|
|
6
|
+
import { type ReportError } from './errors.js';
|
|
7
|
+
/**
|
|
8
|
+
* Mount values or Effect-based view setup in a child resource scope.
|
|
9
|
+
* The surrounding Effect owns the mount; the source keeps its own lifetime.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mount<M, Message = never, E = never, R = never>(parent: Node, definition: View<M, Message> | Effect.Effect<View<M, Message>, E, R>, source: Source<M> & ([Message] extends [never] ? {
|
|
12
|
+
readonly send?: Send<Message>;
|
|
13
|
+
} : {
|
|
14
|
+
readonly send: Send<Message>;
|
|
15
|
+
}), options?: {
|
|
16
|
+
readonly onError?: ReportError;
|
|
17
|
+
}): Effect.Effect<Mounted, E, R | Scope.Scope>;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import * as Exit from 'effect/Exit';
|
|
3
|
+
import * as Scope from 'effect/Scope';
|
|
4
|
+
import { mountViewWithSettlement, unboundSend } from './dom.js';
|
|
5
|
+
import { makeUiRuntime } from './runtime.js';
|
|
6
|
+
import { reportError, reportSafely } from './errors.js';
|
|
7
|
+
import { Settlement } from './settlement.js';
|
|
8
|
+
/**
|
|
9
|
+
* Mount values or Effect-based view setup in a child resource scope.
|
|
10
|
+
* The surrounding Effect owns the mount; the source keeps its own lifetime.
|
|
11
|
+
*/
|
|
12
|
+
export function mount(parent, definition, source, options = {}) {
|
|
13
|
+
return Effect.gen(function* () {
|
|
14
|
+
const lifetime = yield* Effect.acquireRelease(Effect.sync(() => {
|
|
15
|
+
const scope = Scope.makeUnsafe();
|
|
16
|
+
let closingExit;
|
|
17
|
+
const closed = Effect.runSync(Effect.cached(Effect.uninterruptible(Effect.suspend(() => Scope.close(scope, closingExit ?? Exit.void)))));
|
|
18
|
+
const beginClose = (exit) => {
|
|
19
|
+
closingExit ??= exit;
|
|
20
|
+
return closed;
|
|
21
|
+
};
|
|
22
|
+
const close = (exit) => Effect.suspend(() => beginClose(exit));
|
|
23
|
+
return { scope, close, beginClose };
|
|
24
|
+
}), (lifetime, exit) => lifetime.close(exit));
|
|
25
|
+
return yield* Effect.gen(function* () {
|
|
26
|
+
const runtime = yield* makeUiRuntime();
|
|
27
|
+
const view = Effect.isEffect(definition) ? yield* definition : definition;
|
|
28
|
+
const settlement = new Settlement(runtime);
|
|
29
|
+
// Acquisition can throw after starting child cleanup, before a Mounted exists.
|
|
30
|
+
yield* Effect.addFinalizer(() => settlement.wait());
|
|
31
|
+
const mounted = yield* Effect.acquireRelease(Effect.sync(() => mountViewWithSettlement(parent, view, source, { ...options, send: source.send ?? unboundSend }, settlement)), (mounted, exit) => Effect.suspend(() => {
|
|
32
|
+
settlement.exit = exit;
|
|
33
|
+
return mounted.close();
|
|
34
|
+
}));
|
|
35
|
+
const close = lifetime.close(Exit.void);
|
|
36
|
+
const dispose = () => {
|
|
37
|
+
// Claim the exit before user unsubscriptions can reenter application shutdown.
|
|
38
|
+
const closed = lifetime.beginClose(Exit.void);
|
|
39
|
+
mounted.dispose();
|
|
40
|
+
Effect.runFork(closed).addObserver((exit) => {
|
|
41
|
+
if (exit._tag === 'Failure')
|
|
42
|
+
reportSafely(options.onError ?? reportError, exit.cause);
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
return Object.assign(dispose, { dispose, close: () => close });
|
|
46
|
+
}).pipe(Scope.provide(lifetime.scope), Effect.onError((cause) => lifetime.close(Exit.failCause(cause))));
|
|
47
|
+
});
|
|
48
|
+
}
|
package/dist/resource.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Snapshot } from './snapshot.js';
|
|
2
|
+
import type * as Scope from 'effect/Scope';
|
|
2
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
3
4
|
import type { UiLoad } from './load.js';
|
|
4
|
-
import {
|
|
5
|
+
import type { UiRuntime } from './runtime.js';
|
|
5
6
|
import type { View } from './dom.js';
|
|
6
7
|
export interface ResourceModel<Props, A, E = unknown> {
|
|
7
8
|
readonly props: Props;
|
|
@@ -17,11 +18,11 @@ export declare const resourceError: (result: AsyncResult.AsyncResult<unknown, un
|
|
|
17
18
|
export declare function resourceComponent<Props, A, E = unknown, R = never>(definition: {
|
|
18
19
|
request: (props: Snapshot<Props>) => {
|
|
19
20
|
key: string;
|
|
20
|
-
load: () => UiLoad<A, E, R>;
|
|
21
|
+
load: () => UiLoad<A, E, R | Scope.Scope>;
|
|
21
22
|
delay?: number;
|
|
22
23
|
} | undefined;
|
|
23
24
|
view: View<ResourceModel<Props, A, E>, ResourceMessage>;
|
|
24
|
-
} & ([R] extends [never] ? {
|
|
25
|
+
} & ([Exclude<R, Scope.Scope>] extends [never] ? {
|
|
25
26
|
runtime?: UiRuntime<R>;
|
|
26
27
|
} : {
|
|
27
28
|
runtime: UiRuntime<R>;
|
package/dist/resource.js
CHANGED
|
@@ -5,7 +5,6 @@ import * as Option from 'effect/Option';
|
|
|
5
5
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
6
6
|
import { loadEffect } from './load.js';
|
|
7
7
|
import { component } from './component.js';
|
|
8
|
-
import { defaultUiRuntime } from './runtime.js';
|
|
9
8
|
import { effectCommand } from './program.js';
|
|
10
9
|
const commandLoad = commandSlot('load');
|
|
11
10
|
export const available = (result) => Option.getOrUndefined(AsyncResult.value(result));
|
|
@@ -17,7 +16,7 @@ export const resourceError = (result) => {
|
|
|
17
16
|
};
|
|
18
17
|
/** A replaceable request owns one result. Key changes clear it; refreshes preserve the last success. */
|
|
19
18
|
export function resourceComponent(definition) {
|
|
20
|
-
const runtime = definition.runtime
|
|
19
|
+
const runtime = definition.runtime;
|
|
21
20
|
const request = (model, retry = false) => {
|
|
22
21
|
const selected = definition.request(model.props);
|
|
23
22
|
if (!retry && selected?.key === model.key)
|
|
@@ -34,9 +33,14 @@ export function resourceComponent(definition) {
|
|
|
34
33
|
result: AsyncResult.waiting(selected.key === model.key ? model.result : AsyncResult.initial()),
|
|
35
34
|
},
|
|
36
35
|
commands: [
|
|
37
|
-
effectCommand(commandLoad, () =>
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
effectCommand(commandLoad, () => {
|
|
37
|
+
const work = selected.delay
|
|
38
|
+
? Effect.sleep(selected.delay).pipe(Effect.andThen(loadEffect(selected.load)))
|
|
39
|
+
: loadEffect(selected.load);
|
|
40
|
+
return runtime
|
|
41
|
+
? runtime.provideScoped(work)
|
|
42
|
+
: work;
|
|
43
|
+
}, {
|
|
40
44
|
policy: 'replace',
|
|
41
45
|
onSuccess: (value) => ({ type: 'Loaded', value }),
|
|
42
46
|
onFailure: (cause) => ({ type: 'Failed', cause }),
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import type { Snapshot } from './snapshot.js';
|
|
2
2
|
import * as Context from 'effect/Context';
|
|
3
|
+
import * as Clock from 'effect/Clock';
|
|
3
4
|
import * as Effect from 'effect/Effect';
|
|
5
|
+
import type * as Fiber from 'effect/Fiber';
|
|
6
|
+
import * as Scope from 'effect/Scope';
|
|
4
7
|
import { type Command, type RunningProgram, type Transition } from './program.js';
|
|
5
8
|
/** A binding to application-owned services, not a new scope or service lifetime. */
|
|
6
9
|
export interface UiRuntime<R> {
|
|
7
10
|
readonly context: Context.Context<R>;
|
|
11
|
+
readonly clock: Clock.Clock;
|
|
12
|
+
/** Execute work with a fresh resource scope and the captured application context. */
|
|
13
|
+
readonly runFork: <A, E>(effect: Effect.Effect<A, E, R | Scope.Scope>) => Fiber.Fiber<A, E>;
|
|
8
14
|
readonly provide: <A, E>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E>;
|
|
15
|
+
/** Supply services while preserving the resource owner's scope. */
|
|
16
|
+
readonly provideScoped: <A, E>(effect: Effect.Effect<A, E, R | Scope.Scope>) => Effect.Effect<A, E, Scope.Scope>;
|
|
9
17
|
readonly command: <M>(command: Command<M, R>) => Command<M>;
|
|
10
18
|
readonly program: <M, Msg>(options: {
|
|
11
19
|
initial: M | Snapshot<M>;
|
|
@@ -15,4 +23,6 @@ export interface UiRuntime<R> {
|
|
|
15
23
|
}) => RunningProgram<M, Msg>;
|
|
16
24
|
}
|
|
17
25
|
export declare function uiRuntime<R>(context: Context.Context<R>): UiRuntime<R>;
|
|
26
|
+
/** Capture the current Effect environment. The surrounding scope owns all started fibers. */
|
|
27
|
+
export declare const makeUiRuntime: <R = never>() => Effect.Effect<UiRuntime<R>, never, R | Scope.Scope>;
|
|
18
28
|
export declare const defaultUiRuntime: UiRuntime<never>;
|
package/dist/runtime.js
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
import * as Context from 'effect/Context';
|
|
2
|
+
import * as Clock from 'effect/Clock';
|
|
2
3
|
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as FiberSet from 'effect/FiberSet';
|
|
5
|
+
import * as Scope from 'effect/Scope';
|
|
3
6
|
import * as Stream from 'effect/Stream';
|
|
4
7
|
import { program } from './program.js';
|
|
5
8
|
export function uiRuntime(context) {
|
|
9
|
+
return createRuntime(context, Effect.runForkWith(context));
|
|
10
|
+
}
|
|
11
|
+
/** Capture the current Effect environment. The surrounding scope owns all started fibers. */
|
|
12
|
+
export const makeUiRuntime = () => Effect.gen(function* () {
|
|
13
|
+
const context = yield* Effect.context();
|
|
14
|
+
const fibers = yield* FiberSet.make();
|
|
15
|
+
const run = yield* FiberSet.runtime(fibers)();
|
|
16
|
+
return createRuntime(context, run);
|
|
17
|
+
});
|
|
18
|
+
function createRuntime(context, run) {
|
|
6
19
|
const provide = (effect) => Effect.provideContext(effect, context);
|
|
7
20
|
const command = (value) => value.stream
|
|
8
|
-
? {
|
|
21
|
+
? {
|
|
22
|
+
...value,
|
|
23
|
+
stream: Stream.unwrap(Effect.map(Effect.scope, (scope) => value.stream.pipe(Stream.provideContext(Context.add(context, Scope.Scope, scope))))),
|
|
24
|
+
}
|
|
9
25
|
: value.action
|
|
10
|
-
? { ...value, action: provide(value.action) }
|
|
11
|
-
: { ...value, effect: provide(value.effect) };
|
|
26
|
+
? { ...value, action: provide(Effect.scoped(value.action)) }
|
|
27
|
+
: { ...value, effect: provide(Effect.scoped(value.effect)) };
|
|
12
28
|
return {
|
|
13
29
|
context,
|
|
30
|
+
clock: Context.get(context, Clock.Clock),
|
|
31
|
+
runFork: (effect) => run(Effect.scoped(effect)),
|
|
14
32
|
provide,
|
|
33
|
+
provideScoped: (effect) => Effect.flatMap(Effect.scope, (scope) => provide(Scope.provide(effect, scope))),
|
|
15
34
|
command,
|
|
16
35
|
program: (options) => program({
|
|
17
36
|
...options,
|
|
37
|
+
runtime: { runFork: (effect) => run(Effect.scoped(effect)) },
|
|
18
38
|
update: (model, message) => {
|
|
19
39
|
const next = options.update(model, message);
|
|
20
40
|
return {
|
|
@@ -26,4 +46,4 @@ export function uiRuntime(context) {
|
|
|
26
46
|
}),
|
|
27
47
|
};
|
|
28
48
|
}
|
|
29
|
-
export const defaultUiRuntime = uiRuntime(Context.empty());
|
|
49
|
+
export const defaultUiRuntime = /* @__PURE__ */ uiRuntime(Context.empty());
|
package/dist/session.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as Scope from 'effect/Scope';
|
|
1
2
|
import type { Snapshot } from './snapshot.js';
|
|
2
3
|
import { type Query } from './query.js';
|
|
3
4
|
import type { DisposableOwner } from './owner.js';
|
|
@@ -28,8 +29,8 @@ export interface QueryResource<Args, A, E = never> {
|
|
|
28
29
|
export declare function queryResource<Args, A, E, R>(context: {
|
|
29
30
|
cache: QueryCache<R>;
|
|
30
31
|
changed?: () => void;
|
|
31
|
-
}, definition: Query<Args, A, E, NoInfer<R
|
|
32
|
-
export declare function observeQuery<Args, A, E, R>(owner: DisposableOwner, cache: QueryCache<R>, definition: Query<Args, A, E, NoInfer<R
|
|
32
|
+
}, definition: Query<Args, A, E, NoInfer<R> | Scope.Scope>): QueryResource<Args, A, E>;
|
|
33
|
+
export declare function observeQuery<Args, A, E, R>(owner: DisposableOwner, cache: QueryCache<R>, definition: Query<Args, A, E, NoInfer<R> | Scope.Scope>, changed: (result: AsyncResult.AsyncResult<Snapshot<A>, E>) => void): QueryResource<Args, A, E>;
|
|
33
34
|
/** Application selectors run once per explicit input version, with optional prior output sharing. */
|
|
34
35
|
export declare function projectionCache(): {
|
|
35
36
|
invalidate: () => void;
|
package/dist/session.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
2
|
-
import {
|
|
2
|
+
import { encodeQueryArguments } from './query.js';
|
|
3
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
4
4
|
import * as Atom from 'effect/unstable/reactivity/Atom';
|
|
5
5
|
import { cacheInternals } from './cache-internals.js';
|
|
@@ -108,11 +108,14 @@ export function queryResource(context, definition) {
|
|
|
108
108
|
if (disposed || internal.disposed())
|
|
109
109
|
return;
|
|
110
110
|
const nextGeneration = internal.registry.get(internal.generation);
|
|
111
|
-
const nextKey = args === undefined ? undefined :
|
|
111
|
+
const nextKey = args === undefined ? undefined : encodeQueryArguments(definition, args);
|
|
112
112
|
if (nextKey === key && nextGeneration === generation)
|
|
113
113
|
return;
|
|
114
114
|
const selected = ++revision;
|
|
115
115
|
disconnect();
|
|
116
|
+
// Releasing the previous query can run finalizers that select or dispose again.
|
|
117
|
+
if (disposed || selected !== revision)
|
|
118
|
+
return;
|
|
116
119
|
key = nextKey;
|
|
117
120
|
generation = nextGeneration;
|
|
118
121
|
const nextAtom = args === undefined ? undefined : internal.query(definition, args);
|
package/dist/settlement.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/** Internal completion accounting, including reentrant acquisition before a fiber is available. */
|
|
2
2
|
export declare class Settlement {
|
|
3
|
+
readonly runtime?: UiRuntime<never> | undefined;
|
|
4
|
+
constructor(runtime?: UiRuntime<never> | undefined);
|
|
5
|
+
/** Exit of the owning mount; element replacement/removal uses a successful exit. */
|
|
6
|
+
exit: Exit.Exit<unknown, unknown>;
|
|
3
7
|
private pending;
|
|
4
8
|
private waiters;
|
|
5
9
|
begin(): () => void;
|
|
6
10
|
wait(): Effect.Effect<void>;
|
|
7
11
|
}
|
|
8
12
|
import * as Effect from 'effect/Effect';
|
|
13
|
+
import * as Exit from 'effect/Exit';
|
|
14
|
+
import type { UiRuntime } from './runtime.js';
|
package/dist/settlement.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Internal completion accounting, including reentrant acquisition before a fiber is available. */
|
|
2
2
|
export class Settlement {
|
|
3
|
+
runtime;
|
|
4
|
+
constructor(runtime) {
|
|
5
|
+
this.runtime = runtime;
|
|
6
|
+
}
|
|
7
|
+
/** Exit of the owning mount; element replacement/removal uses a successful exit. */
|
|
8
|
+
exit = Exit.void;
|
|
3
9
|
pending = 0;
|
|
4
10
|
waiters = new Set();
|
|
5
11
|
begin() {
|
|
@@ -30,3 +36,4 @@ export class Settlement {
|
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
import * as Effect from 'effect/Effect';
|
|
39
|
+
import * as Exit from 'effect/Exit';
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import type * as Scope from 'effect/Scope';
|
|
3
|
+
import * as Stream from 'effect/Stream';
|
|
4
|
+
import * as SubscriptionRef from 'effect/SubscriptionRef';
|
|
5
|
+
import type * as Atom from 'effect/unstable/reactivity/Atom';
|
|
6
|
+
import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
|
|
7
|
+
import { type Snapshot } from './snapshot.js';
|
|
8
|
+
/** A current immutable value and its publications. Observation does not own the producer. */
|
|
9
|
+
export interface Source<A> {
|
|
10
|
+
readonly model: () => Snapshot<A>;
|
|
11
|
+
readonly subscribe: (listener: (value: Snapshot<A>) => void) => () => void;
|
|
12
|
+
}
|
|
13
|
+
/** Select explicit inputs. Equal results retain their identity and do not notify subscribers. */
|
|
14
|
+
export declare function mapSource<A, B>(source: Source<A>, project: (value: Snapshot<A>) => B | Snapshot<B>, equals?: (previous: Snapshot<B>, next: Snapshot<B>) => boolean): Source<B>;
|
|
15
|
+
/** Observe a snapshot stream in the current Effect scope. Handle typed stream errors as values upstream. */
|
|
16
|
+
export declare const fromStream: <A, R>(stream: Stream.Stream<A, never, R>, initial: A | Snapshot<A>) => Effect.Effect<Source<A>, never, R | Scope.Scope>;
|
|
17
|
+
/** Observe the existing ref; closing the observation leaves the ref and its other users alive. */
|
|
18
|
+
export declare const fromSubscriptionRef: <A>(ref: SubscriptionRef.SubscriptionRef<A>) => Effect.Effect<Source<A>, never, Scope.Scope>;
|
|
19
|
+
export declare function fromAtom<A>(atom: Atom.Atom<A>): Effect.Effect<Source<A>, never, AtomRegistry.AtomRegistry | Scope.Scope>;
|
|
20
|
+
export declare function fromAtom<A>(atom: Atom.Atom<A>, registry: AtomRegistry.AtomRegistry): Effect.Effect<Source<A>, never, Scope.Scope>;
|
|
21
|
+
/** A batched publication boundary for independently owned sessions with explicit invalidation. */
|
|
22
|
+
export declare function projectionSource<Model>(options: {
|
|
23
|
+
invalidate?: () => void;
|
|
24
|
+
refresh?: () => void;
|
|
25
|
+
project: () => Model | Snapshot<Model>;
|
|
26
|
+
reconcile?: (previous: Snapshot<Model>, next: Model | Snapshot<Model>) => Model | Snapshot<Model>;
|
|
27
|
+
afterPublish?: () => void;
|
|
28
|
+
}): {
|
|
29
|
+
readonly disposed: boolean;
|
|
30
|
+
changed: () => void;
|
|
31
|
+
watch(source: {
|
|
32
|
+
subscribe: (changed: () => void) => () => void;
|
|
33
|
+
}): () => void;
|
|
34
|
+
start(): void;
|
|
35
|
+
model(this: void): Snapshot<Model>;
|
|
36
|
+
subscribe(this: void, listener: (model: Snapshot<Model>) => void): () => void;
|
|
37
|
+
dispose(): void;
|
|
38
|
+
};
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import * as Cause from 'effect/Cause';
|
|
3
|
+
import * as Stream from 'effect/Stream';
|
|
4
|
+
import * as SubscriptionRef from 'effect/SubscriptionRef';
|
|
5
|
+
import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
|
|
6
|
+
import { protectSnapshot } from './snapshot.js';
|
|
7
|
+
import { reportError, reportSafely } from './errors.js';
|
|
8
|
+
/** Select explicit inputs. Equal results retain their identity and do not notify subscribers. */
|
|
9
|
+
export function mapSource(source, project, equals = Object.is) {
|
|
10
|
+
let initialized = false;
|
|
11
|
+
let previousInput;
|
|
12
|
+
let current;
|
|
13
|
+
const select = (input) => {
|
|
14
|
+
if (initialized && Object.is(previousInput, input))
|
|
15
|
+
return current;
|
|
16
|
+
const next = protectSnapshot(project(input));
|
|
17
|
+
if (!initialized || !equals(current, next))
|
|
18
|
+
current = next;
|
|
19
|
+
previousInput = input;
|
|
20
|
+
initialized = true;
|
|
21
|
+
return current;
|
|
22
|
+
};
|
|
23
|
+
const model = () => select(source.model());
|
|
24
|
+
return {
|
|
25
|
+
model,
|
|
26
|
+
subscribe(listener) {
|
|
27
|
+
let previous = model();
|
|
28
|
+
return source.subscribe((value) => {
|
|
29
|
+
const next = select(value);
|
|
30
|
+
if (Object.is(previous, next))
|
|
31
|
+
return;
|
|
32
|
+
previous = next;
|
|
33
|
+
listener(next);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function publication(initial) {
|
|
39
|
+
let current = protectSnapshot(initial);
|
|
40
|
+
let disposed = false;
|
|
41
|
+
const listeners = new Set();
|
|
42
|
+
const source = {
|
|
43
|
+
model: () => current,
|
|
44
|
+
subscribe: (listener) => {
|
|
45
|
+
if (disposed)
|
|
46
|
+
return () => { };
|
|
47
|
+
listeners.add(listener);
|
|
48
|
+
return () => {
|
|
49
|
+
listeners.delete(listener);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
source,
|
|
55
|
+
publish(value) {
|
|
56
|
+
if (disposed || Object.is(current, value))
|
|
57
|
+
return;
|
|
58
|
+
current = protectSnapshot(value);
|
|
59
|
+
const next = current;
|
|
60
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- Reentrant subscriptions must not join the publication in progress.
|
|
61
|
+
for (const listener of [...listeners]) {
|
|
62
|
+
if (disposed || current !== next)
|
|
63
|
+
break;
|
|
64
|
+
if (listeners.has(listener)) {
|
|
65
|
+
try {
|
|
66
|
+
listener(next);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
reportSafely(reportError, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
dispose() {
|
|
75
|
+
disposed = true;
|
|
76
|
+
listeners.clear();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Observe a snapshot stream in the current Effect scope. Handle typed stream errors as values upstream. */
|
|
81
|
+
export const fromStream = (stream, initial) => Effect.gen(function* () {
|
|
82
|
+
const state = yield* Effect.acquireRelease(Effect.sync(() => publication(initial)), (state) => Effect.sync(() => state.dispose()));
|
|
83
|
+
yield* Stream.runForEach(stream, (value) => Effect.sync(() => state.publish(value))).pipe(Effect.catchCause((cause) => Effect.sync(() => {
|
|
84
|
+
if (!Cause.hasInterruptsOnly(cause))
|
|
85
|
+
reportSafely(reportError, cause);
|
|
86
|
+
})), Effect.forkScoped({ startImmediately: true }));
|
|
87
|
+
return state.source;
|
|
88
|
+
});
|
|
89
|
+
/** Observe the existing ref; closing the observation leaves the ref and its other users alive. */
|
|
90
|
+
export const fromSubscriptionRef = (ref) => Effect.gen(function* () {
|
|
91
|
+
const initial = yield* SubscriptionRef.get(ref);
|
|
92
|
+
return yield* fromStream(SubscriptionRef.changes(ref), initial);
|
|
93
|
+
});
|
|
94
|
+
export function fromAtom(atom, provided) {
|
|
95
|
+
return Effect.gen(function* () {
|
|
96
|
+
const registry = provided ?? (yield* AtomRegistry.AtomRegistry);
|
|
97
|
+
const state = yield* Effect.acquireRelease(Effect.sync(() => publication(registry.get(atom))), (state) => Effect.sync(() => state.dispose()));
|
|
98
|
+
yield* Effect.acquireRelease(Effect.sync(() => registry.subscribe(atom, (value) => state.publish(value), { immediate: true })), (unsubscribe) => Effect.sync(unsubscribe));
|
|
99
|
+
return state.source;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** A batched publication boundary for independently owned sessions with explicit invalidation. */
|
|
103
|
+
export function projectionSource(options) {
|
|
104
|
+
const listeners = new Set();
|
|
105
|
+
const subscriptions = new Set();
|
|
106
|
+
let published;
|
|
107
|
+
let started = false;
|
|
108
|
+
let disposed = false;
|
|
109
|
+
let queued = false;
|
|
110
|
+
let revision = 0;
|
|
111
|
+
const changed = () => {
|
|
112
|
+
if (disposed)
|
|
113
|
+
return;
|
|
114
|
+
revision++;
|
|
115
|
+
options.invalidate?.();
|
|
116
|
+
if (!started || queued)
|
|
117
|
+
return;
|
|
118
|
+
queued = true;
|
|
119
|
+
queueMicrotask(flush);
|
|
120
|
+
};
|
|
121
|
+
function flush() {
|
|
122
|
+
queued = false;
|
|
123
|
+
if (disposed)
|
|
124
|
+
return;
|
|
125
|
+
options.refresh?.();
|
|
126
|
+
if (disposed)
|
|
127
|
+
return;
|
|
128
|
+
const version = revision;
|
|
129
|
+
const value = options.project();
|
|
130
|
+
const next = protectSnapshot(options.reconcile ? options.reconcile(published, value) : value);
|
|
131
|
+
if (disposed || version !== revision)
|
|
132
|
+
return;
|
|
133
|
+
if (next !== published) {
|
|
134
|
+
published = next;
|
|
135
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- Reentrant subscriptions start with the next publication.
|
|
136
|
+
for (const listener of [...listeners]) {
|
|
137
|
+
if (disposed)
|
|
138
|
+
break;
|
|
139
|
+
if (listeners.has(listener)) {
|
|
140
|
+
try {
|
|
141
|
+
listener(next);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
reportSafely(reportError, error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!disposed)
|
|
150
|
+
options.afterPublish?.();
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
get disposed() {
|
|
154
|
+
return disposed;
|
|
155
|
+
},
|
|
156
|
+
changed,
|
|
157
|
+
watch(source) {
|
|
158
|
+
if (disposed)
|
|
159
|
+
return () => { };
|
|
160
|
+
const unsubscribe = source.subscribe(changed);
|
|
161
|
+
let active = true;
|
|
162
|
+
const stop = () => {
|
|
163
|
+
if (!active)
|
|
164
|
+
return;
|
|
165
|
+
active = false;
|
|
166
|
+
subscriptions.delete(stop);
|
|
167
|
+
unsubscribe();
|
|
168
|
+
};
|
|
169
|
+
if (disposed)
|
|
170
|
+
stop();
|
|
171
|
+
else
|
|
172
|
+
subscriptions.add(stop);
|
|
173
|
+
return stop;
|
|
174
|
+
},
|
|
175
|
+
start() {
|
|
176
|
+
if (disposed || started)
|
|
177
|
+
return;
|
|
178
|
+
options.refresh?.();
|
|
179
|
+
if (disposed)
|
|
180
|
+
return;
|
|
181
|
+
const version = revision;
|
|
182
|
+
published = protectSnapshot(options.project());
|
|
183
|
+
started = true;
|
|
184
|
+
if (version !== revision)
|
|
185
|
+
changed();
|
|
186
|
+
},
|
|
187
|
+
model() {
|
|
188
|
+
if (!started)
|
|
189
|
+
throw new Error('Start projection publication before reading its model');
|
|
190
|
+
return published;
|
|
191
|
+
},
|
|
192
|
+
subscribe(listener) {
|
|
193
|
+
if (disposed)
|
|
194
|
+
return () => { };
|
|
195
|
+
listeners.add(listener);
|
|
196
|
+
return () => {
|
|
197
|
+
listeners.delete(listener);
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
dispose() {
|
|
201
|
+
if (disposed)
|
|
202
|
+
return;
|
|
203
|
+
disposed = true;
|
|
204
|
+
listeners.clear();
|
|
205
|
+
const errors = [];
|
|
206
|
+
for (const stop of [...subscriptions].reverse()) {
|
|
207
|
+
try {
|
|
208
|
+
stop();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
errors.push(error);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (errors.length)
|
|
215
|
+
throw new AggregateError(errors, 'Projection subscription cleanup failed');
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
package/dist/tasks.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Snapshot } from './snapshot.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { TaskPolicy } from './owner.js';
|
|
3
3
|
import * as Effect from 'effect/Effect';
|
|
4
|
+
import type * as Scope from 'effect/Scope';
|
|
4
5
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
5
6
|
import type { View } from './dom.js';
|
|
6
7
|
import { type CommandSlot, type Send, type RunningProgram } from './program.js';
|
|
7
8
|
import { type UiRuntime } from './runtime.js';
|
|
8
9
|
export interface TaskDefinition<Model, Input, A, E, R = never> {
|
|
9
10
|
readonly policy: Exclude<TaskPolicy, 'parallel'>;
|
|
10
|
-
readonly run: (model: Snapshot<Model>, input: Input) => Effect.Effect<A, E, R>;
|
|
11
|
+
readonly run: (model: Snapshot<Model>, input: Input) => Effect.Effect<A, E, R | Scope.Scope>;
|
|
11
12
|
/** Domain identity, compared after fields or props change. Resets this slot only. */
|
|
12
13
|
readonly identity?: (model: Snapshot<Model>) => unknown;
|
|
13
14
|
}
|
|
@@ -55,19 +56,21 @@ interface ControllerTask<R> {
|
|
|
55
56
|
/** Actions sharing a slot share cancellation and concurrency rules. Defaults to a fresh operation identity for this definition. */
|
|
56
57
|
readonly slot?: CommandSlot;
|
|
57
58
|
}
|
|
58
|
-
export declare function defineTasks<Owner extends
|
|
59
|
+
export declare function defineTasks<Owner extends {
|
|
60
|
+
readonly run: (slot: CommandSlot, effect: Effect.Effect<never>, policy: TaskPolicy) => void;
|
|
61
|
+
}, T extends Record<string, ControllerTask<Effect.Services<Parameters<Owner['run']>[1]>>>>(owner: Owner, definitions: T): {
|
|
59
62
|
readonly [K in keyof T]: (...args: Parameters<T[K]['run']>) => void;
|
|
60
63
|
};
|
|
61
64
|
export declare function defineTasks<Props, State extends object, R>(definition: Init<Props, State> & {
|
|
62
65
|
runtime: UiRuntime<R>;
|
|
63
66
|
}): ReturnType<typeof taskBuilder<Props, State, R>>;
|
|
64
67
|
export declare function defineTasks<Props, State extends object>(definition: Init<Props, State>): ReturnType<typeof taskBuilder<Props, State, never>>;
|
|
65
|
-
declare function taskBuilder<Props, State extends object, R>(definition: Init<Props, State>, runtime: UiRuntime<R>): {
|
|
68
|
+
declare function taskBuilder<Props, State extends object, R>(definition: Init<Props, State>, runtime: UiRuntime<R> | undefined): {
|
|
66
69
|
tasks<T extends Definitions<State & {
|
|
67
70
|
readonly props: Props;
|
|
68
71
|
}, R>>(definitions: T): {
|
|
69
72
|
slot: (name: keyof T) => CommandSlot;
|
|
70
|
-
create: (props: Props | Snapshot<Props>) => RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>;
|
|
73
|
+
create: (props: Props | Snapshot<Props>, ownerRuntime?: UiRuntime<never>) => RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>;
|
|
71
74
|
receive: (source: RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>, props: Props | Snapshot<Props>) => void;
|
|
72
75
|
controls: (send: Send<TasksMessage<State, T>>) => {
|
|
73
76
|
run: <K extends keyof T>(task: K, ...input: InputArguments<T[K]>) => void;
|