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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as Scope from 'effect/Scope';
|
|
1
2
|
import type * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
2
3
|
import type * as Atom from 'effect/unstable/reactivity/Atom';
|
|
3
4
|
import type * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
|
|
@@ -7,11 +8,11 @@ import type { Snapshot } from './snapshot.js';
|
|
|
7
8
|
export interface CacheInternals<R> {
|
|
8
9
|
refresh(atom: Atom.Atom<unknown>): void;
|
|
9
10
|
readonly disposed: () => boolean;
|
|
10
|
-
revision<Args, A, E>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): number | undefined;
|
|
11
|
+
revision<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: Args | Snapshot<Args>): number | undefined;
|
|
11
12
|
retain(atom: Atom.Atom<unknown>): () => void;
|
|
12
13
|
readonly registry: AtomRegistry.AtomRegistry;
|
|
13
14
|
readonly generation: Atom.Writable<number>;
|
|
14
|
-
query<Args, A, E>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): Atom.Atom<AsyncResult.AsyncResult<Snapshot<A>, E>>;
|
|
15
|
+
query<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: Args | Snapshot<Args>): Atom.Atom<AsyncResult.AsyncResult<Snapshot<A>, E>>;
|
|
15
16
|
}
|
|
16
17
|
export declare function registerCache<R>(cache: QueryCache<R>, internals: CacheInternals<R>): void;
|
|
17
18
|
/** Private implementation access; deliberately absent from package exports. */
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Snapshot } from './snapshot.js';
|
|
2
2
|
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Scope from 'effect/Scope';
|
|
3
4
|
import { type Query, type QueryGroup } from './query.js';
|
|
4
|
-
import type
|
|
5
|
+
import { type UiRuntime } from './runtime.js';
|
|
5
6
|
export { loadEffect, type UiLoad } from './load.js';
|
|
6
7
|
export { shareValue } from './share.js';
|
|
7
8
|
export interface QueryCacheOptions {
|
|
@@ -10,32 +11,34 @@ export interface QueryCacheOptions {
|
|
|
10
11
|
}
|
|
11
12
|
export declare function makeQueryCache(options?: QueryCacheOptions): QueryCache<never>;
|
|
12
13
|
export declare function makeQueryCache<R>(runtime: UiRuntime<R>, options?: QueryCacheOptions): QueryCache<R>;
|
|
14
|
+
/** Capture application services and clock, and close the cache with the current Effect scope. */
|
|
15
|
+
export declare const scopedQueryCache: <R = never>(options?: QueryCacheOptions) => Effect.Effect<QueryCache<R>, never, R | Scope.Scope>;
|
|
13
16
|
/** A cache owns one registry and the query resources published through it. */
|
|
14
17
|
export interface QueryCache<R = never> {
|
|
15
18
|
batch(work: () => void): void;
|
|
16
|
-
getQueryData<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>): Snapshot<A> | undefined;
|
|
17
|
-
invalidateWhere<Args, A, E>(definition: Query<Args, A, E, R>, predicate: (args: Snapshot<Args>) => boolean): void;
|
|
19
|
+
getQueryData<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>): Snapshot<A> | undefined;
|
|
20
|
+
invalidateWhere<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, predicate: (args: Snapshot<Args>) => boolean): void;
|
|
18
21
|
invalidateGroup(group: QueryGroup): void;
|
|
19
22
|
/** Cancel requests while retaining the last success. Explicit refresh restarts them. */
|
|
20
|
-
cancelQuery<Args, A, E>(definition: Query<Args, A, E, R>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
|
|
23
|
+
cancelQuery<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
|
|
21
24
|
/** Clear cached data and cancel requests; a subsequent selection or refresh reloads. */
|
|
22
|
-
removeQuery<Args, A, E>(definition: Query<Args, A, E, R>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
|
|
23
|
-
prefetch<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, options?: {
|
|
25
|
+
removeQuery<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
|
|
26
|
+
prefetch<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, options?: {
|
|
24
27
|
readonly refresh?: boolean;
|
|
25
28
|
}): Effect.Effect<Snapshot<A>, E>;
|
|
26
29
|
/** Publish a protected success, including undefined, and supersede any pending load for this key. */
|
|
27
|
-
setQueryData<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, value: NoInfer<A> | Snapshot<NoInfer<A>>): Snapshot<A>;
|
|
30
|
+
setQueryData<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, value: NoInfer<A> | Snapshot<NoInfer<A>>): Snapshot<A>;
|
|
28
31
|
/**
|
|
29
32
|
* Update an existing success, including one retained during refresh or failure, and supersede its load.
|
|
30
33
|
* No success or an undefined updater return skips the write. Use setQueryData to seed or store undefined.
|
|
31
34
|
* Updaters run synchronously on readonly data; a throw leaves the cached value unchanged.
|
|
32
35
|
*/
|
|
33
|
-
updateQueryData<Args, A, E>(definition: Query<Args, A, E, R>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, update: (previous: Snapshot<A>) => NoInfer<A> | Snapshot<NoInfer<A>> | undefined): Snapshot<A> | undefined;
|
|
34
|
-
invalidateQuery<Args, A, E>(definition: Query<Args, A, E, R>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
|
|
36
|
+
updateQueryData<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, args: NoInfer<Args> | Snapshot<NoInfer<Args>>, update: (previous: Snapshot<A>) => NoInfer<A> | Snapshot<NoInfer<A>> | undefined): Snapshot<A> | undefined;
|
|
37
|
+
invalidateQuery<Args, A, E>(definition: Query<Args, A, E, R | Scope.Scope>, ...selected: [] | [NoInfer<Args> | Snapshot<NoInfer<Args>>]): void;
|
|
35
38
|
/** Observe account resets without exposing registry mutation. */
|
|
36
39
|
onReset(listener: () => void): () => void;
|
|
37
40
|
resetResources(): void;
|
|
38
|
-
/**
|
|
41
|
+
/** Release retained acquisitions and join all load/resource finalizers, including canceled or evicted entries. */
|
|
39
42
|
close(): Effect.Effect<void>;
|
|
40
43
|
dispose(): void;
|
|
41
44
|
}
|
package/dist/cache.js
CHANGED
|
@@ -2,15 +2,19 @@ import { Settlement } from './settlement.js';
|
|
|
2
2
|
import { protectSnapshot } from './snapshot.js';
|
|
3
3
|
import * as Deferred from 'effect/Deferred';
|
|
4
4
|
import * as Effect from 'effect/Effect';
|
|
5
|
+
import * as Scope from 'effect/Scope';
|
|
6
|
+
import * as Exit from 'effect/Exit';
|
|
5
7
|
import * as Option from 'effect/Option';
|
|
6
8
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
7
9
|
import * as Atom from 'effect/unstable/reactivity/Atom';
|
|
8
10
|
import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
|
|
9
11
|
import { loadEffect } from './load.js';
|
|
10
12
|
import { shareData } from './sharing.js';
|
|
11
|
-
import {
|
|
13
|
+
import { encodeQueryArguments } from './query.js';
|
|
12
14
|
import { registerCache } from './cache-internals.js';
|
|
13
15
|
import { queryDefinition } from './query-internals.js';
|
|
16
|
+
import { defaultUiRuntime, makeUiRuntime } from './runtime.js';
|
|
17
|
+
import { reportError, reportSafely } from './errors.js';
|
|
14
18
|
export { loadEffect } from './load.js';
|
|
15
19
|
export { shareValue } from './share.js';
|
|
16
20
|
export function makeQueryCache(runtimeOrOptions, options = {}) {
|
|
@@ -23,6 +27,7 @@ function createQueryCache(runtime, options = {}) {
|
|
|
23
27
|
if (!Number.isFinite(retention) || retention < 0)
|
|
24
28
|
throw new RangeError('Query retention must be finite and nonnegative.');
|
|
25
29
|
const registry = AtomRegistry.make({ defaultIdleTTL: retention });
|
|
30
|
+
const clock = (runtime ?? defaultUiRuntime).clock;
|
|
26
31
|
const cancelValue = Symbol('cancel');
|
|
27
32
|
const removeValue = Symbol('remove');
|
|
28
33
|
const generation = Atom.keepAlive(Atom.make(0));
|
|
@@ -54,8 +59,28 @@ function createQueryCache(runtime, options = {}) {
|
|
|
54
59
|
next.revision = ++nextRevision;
|
|
55
60
|
next.canceled = false;
|
|
56
61
|
const revision = next.revision;
|
|
62
|
+
const scope = Scope.makeUnsafe();
|
|
63
|
+
const completed = Deferred.makeUnsafe();
|
|
64
|
+
const finish = settlement.begin();
|
|
65
|
+
let started = false;
|
|
66
|
+
let stopping = false;
|
|
67
|
+
get.addFinalizer(() => {
|
|
68
|
+
stopping = true;
|
|
69
|
+
// The atom interrupts its load first. Join acquisition and its ensuring
|
|
70
|
+
// finalizers before closing resources, including late acquisitions.
|
|
71
|
+
Effect.runFork(Effect.uninterruptible(Effect.gen(function* () {
|
|
72
|
+
if (started)
|
|
73
|
+
yield* Deferred.await(completed);
|
|
74
|
+
yield* Scope.close(scope, Exit.void);
|
|
75
|
+
})).pipe(Effect.ensuring(Effect.sync(finish)))).addObserver((exit) => {
|
|
76
|
+
if (Exit.isFailure(exit))
|
|
77
|
+
reportSafely(reportError, exit.cause);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
57
80
|
return Effect.suspend(() => {
|
|
58
|
-
|
|
81
|
+
if (stopping)
|
|
82
|
+
return Effect.interrupt;
|
|
83
|
+
started = true;
|
|
59
84
|
return loadEffect(() => next.load()).pipe(Effect.map((value) => {
|
|
60
85
|
const shared = Option.isSome(previous)
|
|
61
86
|
? share(previous.value, value)
|
|
@@ -64,7 +89,12 @@ function createQueryCache(runtime, options = {}) {
|
|
|
64
89
|
if (!disposed && revision === next.revision)
|
|
65
90
|
remember(next, snapshot);
|
|
66
91
|
return snapshot;
|
|
67
|
-
}), Effect.ensuring(Effect.sync(
|
|
92
|
+
}), Scope.provide(scope), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void).pipe(Effect.ensuring(Effect.sync(() => {
|
|
93
|
+
Deferred.doneUnsafe(completed, Effect.void);
|
|
94
|
+
// Successful loads retain their resources with the atom entry.
|
|
95
|
+
if (Exit.isFailure(exit))
|
|
96
|
+
finish();
|
|
97
|
+
})))));
|
|
68
98
|
});
|
|
69
99
|
});
|
|
70
100
|
const next = {
|
|
@@ -112,7 +142,7 @@ function createQueryCache(runtime, options = {}) {
|
|
|
112
142
|
return { entry, atom: entry.atom };
|
|
113
143
|
};
|
|
114
144
|
const remember = (entry, value) => {
|
|
115
|
-
entry.loadedAt =
|
|
145
|
+
entry.loadedAt = clock.currentTimeMillisUnsafe();
|
|
116
146
|
const node = registry.getNodes().get(entry.atom);
|
|
117
147
|
if (node)
|
|
118
148
|
values.set(node, { value });
|
|
@@ -125,13 +155,15 @@ function createQueryCache(runtime, options = {}) {
|
|
|
125
155
|
if (disposed)
|
|
126
156
|
throw new Error('Cannot write to a disposed query cache.');
|
|
127
157
|
};
|
|
128
|
-
const queryKey = (definition, args) => `query:${identity(definition)}:${
|
|
158
|
+
const queryKey = (definition, args) => `query:${identity(definition)}:${encodeQueryArguments(definition, args)}`;
|
|
129
159
|
const acquireQuery = (definition, args) => {
|
|
130
160
|
const config = queryDefinition(definition);
|
|
131
161
|
protectSnapshot(args);
|
|
132
162
|
const { entry, atom } = acquire(queryKey(definition, args), () => {
|
|
133
163
|
const effect = Effect.suspend(() => config.load(args, previousValue(resources.get(queryKey(definition, args)))?.value));
|
|
134
|
-
return runtime
|
|
164
|
+
return runtime
|
|
165
|
+
? runtime.provideScoped(effect)
|
|
166
|
+
: effect;
|
|
135
167
|
}, config.share);
|
|
136
168
|
entry.query = definition;
|
|
137
169
|
entry.args = args;
|
|
@@ -149,7 +181,7 @@ function createQueryCache(runtime, options = {}) {
|
|
|
149
181
|
refresh(entry);
|
|
150
182
|
if (registry.getNodes().has(atom) &&
|
|
151
183
|
entry.loadedAt !== undefined &&
|
|
152
|
-
|
|
184
|
+
clock.currentTimeMillisUnsafe() - entry.loadedAt >= config.staleTime) {
|
|
153
185
|
const current = registry.get(atom);
|
|
154
186
|
if (!current.waiting)
|
|
155
187
|
refresh(entry);
|
|
@@ -326,3 +358,8 @@ function createQueryCache(runtime, options = {}) {
|
|
|
326
358
|
});
|
|
327
359
|
return Object.freeze(cache);
|
|
328
360
|
}
|
|
361
|
+
/** Capture application services and clock, and close the cache with the current Effect scope. */
|
|
362
|
+
export const scopedQueryCache = (options = {}) => Effect.gen(function* () {
|
|
363
|
+
const runtime = yield* makeUiRuntime();
|
|
364
|
+
return yield* Effect.acquireRelease(Effect.sync(() => makeQueryCache(runtime, options)), (cache) => cache.close());
|
|
365
|
+
});
|
package/dist/component.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare function component<Props, Model extends {
|
|
|
31
31
|
/** Mount an existing program without introducing a second state owner. */
|
|
32
32
|
export declare function programView<Props, Model, Message>(definition: {
|
|
33
33
|
identity?: (props: Snapshot<Props>) => unknown;
|
|
34
|
-
create: (props: Snapshot<Props>) => import('./program').Program<Model, Message>;
|
|
34
|
+
create: (props: Snapshot<Props>, runtime: UiRuntime<never>) => import('./program').Program<Model, Message>;
|
|
35
35
|
receive: (source: import('./program').Program<Model, Message>, props: Snapshot<Props>) => void;
|
|
36
36
|
view: View<Model, Message>;
|
|
37
37
|
}): View<Props, never>;
|
package/dist/component.js
CHANGED
|
@@ -19,8 +19,9 @@ export function localComponent(definition) {
|
|
|
19
19
|
}
|
|
20
20
|
/** A child model receives immutable parent inputs and owns its own message/command loop. */
|
|
21
21
|
export function component(definition) {
|
|
22
|
-
const runtime = definition.runtime ?? defaultUiRuntime;
|
|
23
22
|
return identify(compiled((scope, parent, before) => {
|
|
23
|
+
const ownerRuntime = scope.settlement.runtime ?? defaultUiRuntime;
|
|
24
|
+
const runtime = definition.runtime ?? ownerRuntime;
|
|
24
25
|
const wrap = (next) => ({
|
|
25
26
|
model: next.model,
|
|
26
27
|
...(next.cancel ? { cancel: next.cancel } : {}),
|
|
@@ -33,6 +34,7 @@ export function component(definition) {
|
|
|
33
34
|
const source = program({
|
|
34
35
|
initial: definition.init(scope.value),
|
|
35
36
|
onDefect: scope.report,
|
|
37
|
+
runtime: ownerRuntime,
|
|
36
38
|
update: (model, envelope) => wrap(envelope.type === 'Input'
|
|
37
39
|
? (definition.receive?.(model, envelope.props) ?? {
|
|
38
40
|
model: Object.is(model.props, envelope.props)
|
|
@@ -62,7 +64,7 @@ export function component(definition) {
|
|
|
62
64
|
/** Mount an existing program without introducing a second state owner. */
|
|
63
65
|
export function programView(definition) {
|
|
64
66
|
return identify(compiled((scope, parent, before) => {
|
|
65
|
-
const source = definition.create(scope.value);
|
|
67
|
+
const source = definition.create(scope.value, scope.settlement.runtime ?? defaultUiRuntime);
|
|
66
68
|
if (scope.disposed) {
|
|
67
69
|
closeProgram(scope, source);
|
|
68
70
|
return;
|
package/dist/dom.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as Effect from 'effect/Effect';
|
|
2
|
-
import type
|
|
2
|
+
import { type Snapshot } from './snapshot.js';
|
|
3
3
|
import { type ReportError } from './errors.js';
|
|
4
4
|
import { type BindingSource } from './diagnostics.js';
|
|
5
5
|
import type { Rows } from './index.js';
|
|
6
6
|
import { jsxComponent, type JSX } from './jsx.js';
|
|
7
7
|
import type { DomMount } from './mount.js';
|
|
8
8
|
import { Settlement } from './settlement.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { Send } from './program.js';
|
|
10
|
+
import type { Source } from './source.js';
|
|
11
|
+
import type { UiRuntime } from './runtime.js';
|
|
10
12
|
type Dependencies = () => readonly unknown[];
|
|
11
13
|
type BindingLocation = BindingSource | (() => BindingSource | undefined);
|
|
12
14
|
type Cleanup = () => void;
|
|
@@ -38,7 +40,7 @@ export declare const ViewBinding: (<M, E>(this: never, props: {
|
|
|
38
40
|
[jsxComponent]: true;
|
|
39
41
|
};
|
|
40
42
|
/** Untyped callers get an actionable failure instead of silently losing child messages. */
|
|
41
|
-
export declare function unboundSend(_message:
|
|
43
|
+
export declare function unboundSend(_message: unknown): never;
|
|
42
44
|
declare const contentBrand: unique symbol;
|
|
43
45
|
/** Owned markup accepted wherever JSX can render content. */
|
|
44
46
|
export interface CompiledContent {
|
|
@@ -52,6 +54,8 @@ export declare function slot<A = void>(render: (value: Snapshot<A>) => JSX.Eleme
|
|
|
52
54
|
/** Evaluate ordinary synchronous JavaScript for each immutable model publication. */
|
|
53
55
|
export declare function view<M, E = never>(render: (model: Snapshot<M>, send: Send<E>) => JSX.Element): View<M, E>;
|
|
54
56
|
export declare function compiled<M, E>(build: Build<M, E>): View<M, E>;
|
|
57
|
+
/** Skip a view only under the caller's explicit equality contract. */
|
|
58
|
+
export declare function memoView<M, E>(definition: View<M, E>, equals: (previous: Snapshot<M>, next: Snapshot<M>) => boolean): View<M, E>;
|
|
55
59
|
type MarkupProps = Readonly<Record<string, unknown>> & {
|
|
56
60
|
readonly children?: JSX.Element;
|
|
57
61
|
};
|
|
@@ -60,18 +64,44 @@ export declare function markup(tag: string, sources?: Readonly<Record<string, Bi
|
|
|
60
64
|
export declare function renderComponent(component: (props: never) => JSX.Element, props: Readonly<Record<string, unknown>>): JSX.Element;
|
|
61
65
|
/** Preserve collection identity (or array reference identity) while rendering rows. */
|
|
62
66
|
export declare function list<A>(rows: Rows<A> | readonly A[], render: (item: A, index: number) => JSX.Element): JSX.Element;
|
|
67
|
+
/**
|
|
68
|
+
* Define keyed row projection once. Every dependency is passed as an explicit input;
|
|
69
|
+
* only changed projected props enter the row view's renderer.
|
|
70
|
+
*/
|
|
71
|
+
export declare function listView<A, Input, Props, Message = never>(options: {
|
|
72
|
+
readonly project: (item: A, index: number, input: Input) => Props | Snapshot<Props>;
|
|
73
|
+
readonly view: View<Props, Message>;
|
|
74
|
+
readonly equals?: (previous: Snapshot<Props>, next: Snapshot<Props>) => boolean;
|
|
75
|
+
}): (rows: Rows<A> | readonly A[], input: Input, ...dispatch: [Message] extends [never] ? [send?: Send<Message>] : [send: Send<Message>]) => JSX.Element;
|
|
76
|
+
/** Render a declared source in its own subscribed region. No dependencies are inferred. */
|
|
77
|
+
export declare function observe<A>(source: Source<A>, render: (value: Snapshot<A>) => JSX.Element): JSX.Element;
|
|
63
78
|
export interface PortalProps {
|
|
64
79
|
readonly children?: JSX.Element;
|
|
65
80
|
readonly mount?: Element | undefined;
|
|
66
81
|
}
|
|
67
82
|
/** Owned content in an element target, defaulting to the document body. */
|
|
68
83
|
export declare const Portal: View<PortalProps, never>;
|
|
69
|
-
export
|
|
70
|
-
onError?: ReportError;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
export interface MountOptions {
|
|
85
|
+
readonly onError?: ReportError;
|
|
86
|
+
readonly runtime?: UiRuntime<never>;
|
|
87
|
+
}
|
|
88
|
+
export interface Mounted {
|
|
89
|
+
(): void;
|
|
90
|
+
readonly dispose: () => void;
|
|
91
|
+
readonly close: () => Effect.Effect<void>;
|
|
92
|
+
}
|
|
93
|
+
export declare function mountView<M, E>(parent: Node, definition: View<M, E>, source: Source<M> & {
|
|
94
|
+
readonly send: Send<E>;
|
|
95
|
+
}, options?: MountOptions): Mounted;
|
|
96
|
+
export declare function mountView<M>(parent: Node, definition: View<M, never>, source: Source<M>, options?: MountOptions): Mounted;
|
|
97
|
+
export declare function mountView<M, E>(parent: Node, definition: View<M, E>, source: Source<M>, options: MountOptions & {
|
|
98
|
+
readonly send: Send<E>;
|
|
99
|
+
}): Mounted;
|
|
100
|
+
/** Internal mounting boundary. Its owner retains completion accounting if construction fails. */
|
|
101
|
+
export declare function mountViewWithSettlement<M, E>(parent: Node, definition: View<M, E>, source: Source<M>, options: {
|
|
102
|
+
readonly send: Send<E>;
|
|
103
|
+
readonly onError?: ReportError;
|
|
104
|
+
}, settlement: Settlement): Mounted;
|
|
75
105
|
export declare function element(parent: Node, before: Node | null, tag: string): Element;
|
|
76
106
|
export declare function attribute(element: Element, name: string, value: unknown): void;
|
|
77
107
|
/** Compiler output for attributes whose unchanged value needs no DOM refresh. */
|
package/dist/dom.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import attributeData from './dom-attributes.json' with { type: 'json' };
|
|
2
2
|
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Exit from 'effect/Exit';
|
|
4
|
+
import { protectSnapshot } from './snapshot.js';
|
|
3
5
|
import { validateIdentities } from './collection.js';
|
|
4
6
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
5
7
|
import { eventEffects } from './effectEvent.js';
|
|
@@ -130,6 +132,18 @@ export function compiled(build) {
|
|
|
130
132
|
const definition = Object.assign((props) => new SlotPlacement(viewContent(definition), { model: props, send: unboundSend }), { build, [jsxComponent]: true });
|
|
131
133
|
return definition;
|
|
132
134
|
}
|
|
135
|
+
/** Skip a view only under the caller's explicit equality contract. */
|
|
136
|
+
export function memoView(definition, equals) {
|
|
137
|
+
return compiled((scope, parent, before) => {
|
|
138
|
+
const child = new Scope(scope.value, scope.send, scope.report, scope.settlement);
|
|
139
|
+
scope.cleanups.push(() => child.dispose());
|
|
140
|
+
definition.build(child, parent, before);
|
|
141
|
+
scope.jobs.push(() => {
|
|
142
|
+
if (!equals(child.value, scope.value))
|
|
143
|
+
child.set(scope.value);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
133
147
|
const viewDefinitions = new WeakMap();
|
|
134
148
|
function viewContent(definition) {
|
|
135
149
|
let content = viewDefinitions.get(definition);
|
|
@@ -195,6 +209,63 @@ const listDefinition = contentDefinition((scope, parent, before) => {
|
|
|
195
209
|
export function list(rows, render) {
|
|
196
210
|
return new SlotPlacement(listDefinition, { rows, render });
|
|
197
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Define keyed row projection once. Every dependency is passed as an explicit input;
|
|
214
|
+
* only changed projected props enter the row view's renderer.
|
|
215
|
+
*/
|
|
216
|
+
export function listView(options) {
|
|
217
|
+
const equals = options.equals ?? Object.is;
|
|
218
|
+
const definition = contentDefinition((scope, parent, before) => {
|
|
219
|
+
each(scope, parent, before, () => scope.value.rows, () => [scope.value.input, scope.value.send], true, (row, parent, before) => {
|
|
220
|
+
const model = () => protectSnapshot(options.project(row.value[0], row.value[1], scope.value.input));
|
|
221
|
+
const child = new Scope(model(), (message) => scope.value.send(message), scope.report, scope.settlement);
|
|
222
|
+
row.cleanups.push(() => child.dispose());
|
|
223
|
+
options.view.build(child, parent, before);
|
|
224
|
+
row.jobs.push(() => {
|
|
225
|
+
const next = model();
|
|
226
|
+
if (!equals(child.value, next))
|
|
227
|
+
child.set(next);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
return (rows, input, ...dispatch) => new SlotPlacement(definition, { rows, input, send: dispatch[0] ?? unboundSend });
|
|
232
|
+
}
|
|
233
|
+
const observationDefinition = contentDefinition((scope, parent, before) => {
|
|
234
|
+
let unsubscribe = () => { };
|
|
235
|
+
let source = scope.value.source;
|
|
236
|
+
const child = new Scope(source.model(), unboundSend, scope.report, scope.settlement);
|
|
237
|
+
scope.cleanups.push(() => child.dispose(), () => unsubscribe());
|
|
238
|
+
text(child, parent, before, () => [child.value, scope.value.render], () => scope.value.render(child.value));
|
|
239
|
+
const connect = () => {
|
|
240
|
+
if (scope.disposed)
|
|
241
|
+
return;
|
|
242
|
+
const current = source;
|
|
243
|
+
const release = current.subscribe((value) => {
|
|
244
|
+
if (!scope.disposed && source === current)
|
|
245
|
+
child.set(value);
|
|
246
|
+
});
|
|
247
|
+
if (scope.disposed || source !== current)
|
|
248
|
+
release();
|
|
249
|
+
else {
|
|
250
|
+
unsubscribe = release;
|
|
251
|
+
child.set(current.model());
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
connect();
|
|
255
|
+
scope.jobs.push(() => {
|
|
256
|
+
if (source !== scope.value.source) {
|
|
257
|
+
unsubscribe();
|
|
258
|
+
source = scope.value.source;
|
|
259
|
+
connect();
|
|
260
|
+
}
|
|
261
|
+
else
|
|
262
|
+
child.set(source.model());
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
/** Render a declared source in its own subscribed region. No dependencies are inferred. */
|
|
266
|
+
export function observe(source, render) {
|
|
267
|
+
return new SlotPlacement(observationDefinition, { source, render });
|
|
268
|
+
}
|
|
198
269
|
/** Owned content in an element target, defaulting to the document body. */
|
|
199
270
|
export const Portal = /* @__PURE__ */ compiled((scope) => {
|
|
200
271
|
portal(scope, (child, parent, before) => {
|
|
@@ -261,9 +332,14 @@ function buildFragment(parent) {
|
|
|
261
332
|
return fragment;
|
|
262
333
|
}
|
|
263
334
|
export function mountView(parent, definition, source, options = {}) {
|
|
335
|
+
return mountViewWithSettlement(parent, definition, source, { ...options, send: options.send ?? source.send ?? unboundSend }, new Settlement(options.runtime));
|
|
336
|
+
}
|
|
337
|
+
/** Internal mounting boundary. Its owner retains completion accounting if construction fails. */
|
|
338
|
+
export function mountViewWithSettlement(parent, definition, source, options, settlement) {
|
|
264
339
|
return commitDom(() => {
|
|
340
|
+
const initial = source.model();
|
|
265
341
|
const { start, end } = markers(parent, null);
|
|
266
|
-
const scope = new Scope(
|
|
342
|
+
const scope = new Scope(initial, options.send, options.onError, settlement);
|
|
267
343
|
let unsubscribe = () => { };
|
|
268
344
|
let ready = false;
|
|
269
345
|
let latest = scope.value;
|
|
@@ -281,13 +357,22 @@ export function mountView(parent, definition, source, options = {}) {
|
|
|
281
357
|
scope.set(latest);
|
|
282
358
|
}
|
|
283
359
|
catch (error) {
|
|
360
|
+
settlement.exit = Exit.die(error);
|
|
284
361
|
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
285
362
|
throw error;
|
|
286
363
|
}
|
|
364
|
+
let disposed = false;
|
|
287
365
|
const dispose = () => {
|
|
288
|
-
if (
|
|
366
|
+
if (disposed)
|
|
289
367
|
return;
|
|
290
|
-
|
|
368
|
+
disposed = true;
|
|
369
|
+
const finish = settlement.begin();
|
|
370
|
+
try {
|
|
371
|
+
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
finish();
|
|
375
|
+
}
|
|
291
376
|
};
|
|
292
377
|
return Object.assign(dispose, {
|
|
293
378
|
dispose,
|
|
@@ -357,7 +442,9 @@ export function attribute(element, name, value) {
|
|
|
357
442
|
style.setProperty(property, value);
|
|
358
443
|
styleProperties.set(element, next);
|
|
359
444
|
}
|
|
360
|
-
else if (name === 'value' && 'value' in element) {
|
|
445
|
+
else if (name === 'value' && 'value' in element && element.localName !== 'option') {
|
|
446
|
+
// An <option> gets its value as an attribute: before its text child exists its value property reads "", so a
|
|
447
|
+
// property write of "" would be skipped and the option would later fall back to its text.
|
|
361
448
|
const next = scalar(value);
|
|
362
449
|
if (element.value !== next)
|
|
363
450
|
element.value = next;
|
|
@@ -995,7 +1082,7 @@ export function attach(scope, element, dependencies, read) {
|
|
|
995
1082
|
let active;
|
|
996
1083
|
scope.cleanups.push(() => {
|
|
997
1084
|
generation++;
|
|
998
|
-
active?.dispose();
|
|
1085
|
+
active?.dispose(scope.settlement.exit);
|
|
999
1086
|
active = undefined;
|
|
1000
1087
|
});
|
|
1001
1088
|
scope.watch(dependencies, () => {
|
|
@@ -1011,7 +1098,7 @@ export function attach(scope, element, dependencies, read) {
|
|
|
1011
1098
|
if (!scope.disposed && token === generation) {
|
|
1012
1099
|
const finished = scope.settlement.begin();
|
|
1013
1100
|
try {
|
|
1014
|
-
const acquired = prepareMount(element, mount, scope.report);
|
|
1101
|
+
const acquired = prepareMount(element, mount, scope.report, scope.settlement.runtime);
|
|
1015
1102
|
Effect.runFork(acquired.closed).addObserver(finished);
|
|
1016
1103
|
if (scope.disposed || token !== generation)
|
|
1017
1104
|
acquired.dispose();
|
package/dist/effectEvent.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import * as Effect from 'effect/Effect';
|
|
2
|
+
import type * as Scope from 'effect/Scope';
|
|
2
3
|
import { type ReportError } from './errors.js';
|
|
3
4
|
import type { Settlement } from './settlement.js';
|
|
4
5
|
import { type UiRuntime } from './runtime.js';
|
|
5
6
|
declare const tag: unique symbol;
|
|
6
7
|
export interface EffectEventRequest {
|
|
7
8
|
readonly [tag]: true;
|
|
8
|
-
readonly policy: 'drop' | 'replace';
|
|
9
|
-
readonly effect: Effect.Effect<unknown, unknown>;
|
|
9
|
+
readonly policy: 'drop' | 'replace' | 'parallel';
|
|
10
|
+
readonly effect: Effect.Effect<unknown, unknown, Scope.Scope>;
|
|
10
11
|
}
|
|
11
12
|
/** Capture/prevent native events on every call; the policy gates only Effect execution. */
|
|
12
|
-
export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E>): (event: EventType) => EffectEventRequest;
|
|
13
|
-
export declare function effectEvent<EventType extends Event, E, R>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E, R>, runtime: UiRuntime<R>): (event: EventType) => EffectEventRequest;
|
|
13
|
+
export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E, Scope.Scope>): (event: EventType) => EffectEventRequest;
|
|
14
|
+
export declare function effectEvent<EventType extends Event, E, R>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E, R | Scope.Scope>, runtime: UiRuntime<R>): (event: EventType) => EffectEventRequest;
|
|
15
|
+
/** Capture an operation's services in Effect setup; its event listener supplies the work scope. */
|
|
16
|
+
export declare const makeEffectHandler: <Args extends readonly unknown[], A, E, R = never>(work: (...args: Args) => Effect.Effect<A, E, R | Scope.Scope>) => Effect.Effect<(...args: Args) => Effect.Effect<A, E, Scope.Scope>, never, R>;
|
|
14
17
|
/** Internal listener owner, allocated only when an event returns an owned request. */
|
|
15
18
|
export declare function eventEffects(report: ReportError, settlement?: Settlement): {
|
|
16
|
-
accept(
|
|
19
|
+
accept(value: EffectEventRequest | Effect.Effect<unknown, unknown, Scope.Scope>): void;
|
|
17
20
|
dispose(): void;
|
|
18
21
|
};
|
|
19
22
|
export {};
|
package/dist/effectEvent.js
CHANGED
|
@@ -2,51 +2,67 @@ import * as Cause from 'effect/Cause';
|
|
|
2
2
|
import * as Effect from 'effect/Effect';
|
|
3
3
|
import * as Fiber from 'effect/Fiber';
|
|
4
4
|
import { reportSafely } from './errors.js';
|
|
5
|
-
import {
|
|
5
|
+
import { uiRuntime } from './runtime.js';
|
|
6
6
|
const tag = Symbol('Effect event');
|
|
7
7
|
export function effectEvent(policy, load, runtime) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
return (event) => ({
|
|
9
|
+
[tag]: true,
|
|
10
|
+
policy,
|
|
11
|
+
effect: runtime
|
|
12
|
+
? runtime.provideScoped(load(event))
|
|
13
|
+
: load(event),
|
|
14
|
+
});
|
|
10
15
|
}
|
|
16
|
+
/** Capture an operation's services in Effect setup; its event listener supplies the work scope. */
|
|
17
|
+
export const makeEffectHandler = (work) => Effect.map(Effect.context(), (context) => {
|
|
18
|
+
const runtime = uiRuntime(context);
|
|
19
|
+
return (...args) => runtime.provideScoped(Effect.suspend(() => work(...args)));
|
|
20
|
+
});
|
|
11
21
|
/** Internal listener owner, allocated only when an event returns an owned request. */
|
|
12
22
|
export function eventEffects(report, settlement) {
|
|
13
|
-
|
|
23
|
+
const active = new Set();
|
|
14
24
|
let disposed = false;
|
|
15
25
|
const stop = () => {
|
|
16
|
-
const previous = active;
|
|
17
|
-
active
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
const previous = [...active];
|
|
27
|
+
active.clear();
|
|
28
|
+
for (const token of previous)
|
|
29
|
+
if (token.fiber)
|
|
30
|
+
Effect.runFork(Fiber.interrupt(token.fiber));
|
|
20
31
|
};
|
|
21
32
|
return {
|
|
22
|
-
accept(
|
|
33
|
+
accept(value) {
|
|
34
|
+
// Direct Effects admit every event. Other policies are explicit requests.
|
|
35
|
+
const request = Effect.isEffect(value)
|
|
36
|
+
? { policy: 'parallel', effect: value }
|
|
37
|
+
: value;
|
|
23
38
|
if (disposed)
|
|
24
39
|
return;
|
|
25
|
-
if (request.policy === 'drop' && active)
|
|
40
|
+
if (request.policy === 'drop' && active.size)
|
|
26
41
|
return;
|
|
27
|
-
|
|
42
|
+
if (request.policy === 'replace')
|
|
43
|
+
stop();
|
|
28
44
|
const token = {};
|
|
29
|
-
active
|
|
45
|
+
active.add(token);
|
|
30
46
|
const finished = settlement?.begin();
|
|
31
47
|
try {
|
|
32
|
-
const fiber =
|
|
48
|
+
const fiber = settlement?.runtime
|
|
49
|
+
? settlement.runtime.runFork(request.effect)
|
|
50
|
+
: Effect.runFork(Effect.scoped(request.effect));
|
|
33
51
|
token.fiber = fiber;
|
|
34
52
|
fiber.addObserver((exit) => {
|
|
35
|
-
const current = !disposed && active
|
|
36
|
-
|
|
37
|
-
active = undefined;
|
|
53
|
+
const current = !disposed && active.has(token);
|
|
54
|
+
active.delete(token);
|
|
38
55
|
if (exit._tag === 'Failure' && (current || !Cause.hasInterruptsOnly(exit.cause)))
|
|
39
56
|
reportSafely(report, exit.cause);
|
|
40
57
|
finished?.();
|
|
41
58
|
});
|
|
42
|
-
if (disposed || active
|
|
59
|
+
if (disposed || !active.has(token)) {
|
|
43
60
|
Effect.runFork(Fiber.interrupt(fiber));
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
catch (error) {
|
|
47
64
|
finished?.();
|
|
48
|
-
|
|
49
|
-
active = undefined;
|
|
65
|
+
active.delete(token);
|
|
50
66
|
reportSafely(report, error);
|
|
51
67
|
}
|
|
52
68
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
export type { Snapshot, SnapshotOpaque, snapshotOpaque } from './snapshot.js';
|
|
2
|
-
export { modelOwner, type ModelOwner, type DisposableOwner, type TaskPolicy } from './owner.js';
|
|
2
|
+
export { modelOwner, makeModelOwner, type ModelOwner, type DisposableOwner, type TaskPolicy, } from './owner.js';
|
|
3
3
|
export { collection, entities, sequence, type Rows, type Collection } from './collection.js';
|
|
4
4
|
export { component, localComponent, programView } from './component.js';
|
|
5
5
|
export { patchModel } from './state.js';
|
|
6
6
|
export { defineActions, type ActionMessage } from './actions.js';
|
|
7
7
|
export { taskComponent, taskControls, type TaskModel, type TaskMessage } from './task.js';
|
|
8
|
-
export { effectEvent } from './effectEvent.js';
|
|
9
|
-
export { mountView, view, list, ViewBinding, type View } from './dom.js';
|
|
8
|
+
export { effectEvent, makeEffectHandler } from './effectEvent.js';
|
|
9
|
+
export { mountView, view, list, listView, memoView, observe, ViewBinding, type View, type Mounted, } from './dom.js';
|
|
10
|
+
export { mount } from './render.js';
|
|
11
|
+
export { mapSource, fromStream, fromSubscriptionRef, fromAtom, projectionSource, type Source, } from './source.js';
|
|
10
12
|
export { lazyView, type LazyViewOptions } from './lazy.js';
|
|
11
13
|
export type { JSX } from './jsx.js';
|
|
12
|
-
export { Portal, domBinding, domMount, type DomMount, type PortalProps } from './mount.js';
|
|
14
|
+
export { Portal, domBinding, domMount, makeDomMount, makeDomBinding, type DomMount, type PortalProps, } from './mount.js';
|
|
13
15
|
export { pages, type PagesMessage, type PagesRequest, type PagesModel, type PagesProgram, type Pagination, } from './pages.js';
|
|
14
|
-
export { program, mapTransition, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
|
|
16
|
+
export { program, makeProgram, mapTransition, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
|
|
15
17
|
export { available, resourceComponent, resourceError, type ResourceMessage, type ResourceModel, } from './resource.js';
|
|
16
18
|
export { fromPromise, type UiLoad, type UiPage } from './load.js';
|
|
17
19
|
export { observeBindings, inspectBindings, mountBindingInspector, type BindingInspector, type BindingInspection, observePrograms, type BindingUpdate, type ProgramUpdate, } from './diagnostics.js';
|
|
@@ -19,9 +21,9 @@ export { inputText, inputChecked, inputNumber, submit, defineField, type FieldRe
|
|
|
19
21
|
export { slot, type Slot, type CompiledContent } from './dom.js';
|
|
20
22
|
export { AsyncContent, type AsyncContentProps } from './AsyncContent.js';
|
|
21
23
|
export { defineTasks, type TaskDefinition, type TasksModel, type TasksMessage, type TaskResults, } from './tasks.js';
|
|
22
|
-
export { uiRuntime, type UiRuntime } from './runtime.js';
|
|
23
|
-
export { query, queryGroup, type Query, type QueryKey, type QueryGroup } from './query.js';
|
|
24
|
-
export { makeQueryCache, type QueryCache, type QueryCacheOptions } from './cache.js';
|
|
24
|
+
export { uiRuntime, makeUiRuntime, type UiRuntime } from './runtime.js';
|
|
25
|
+
export { query, queryGroup, type Query, type QueryKey, type QueryGroup, type QueryEncoding, } from './query.js';
|
|
26
|
+
export { makeQueryCache, scopedQueryCache, type QueryCache, type QueryCacheOptions, } from './cache.js';
|
|
25
27
|
export { queryResource, type QueryResource, observeQuery, lifetime, projectionCache, sessionGroup, type SessionContext, type Read, } from './session.js';
|
|
26
28
|
export { httpJson } from './http.js';
|
|
27
29
|
export { infiniteQuery, infiniteResource, type InfiniteData, type InfiniteQuery, type InfiniteResource, } from './infinite-query.js';
|