effectweb 0.3.0 → 0.4.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/README.md +35 -4
- package/dist/AsyncContent.d.ts +1 -1
- package/dist/AsyncContent.js +18 -35
- package/dist/boundary.d.ts +11 -0
- package/dist/boundary.js +66 -0
- package/dist/cache-internals.d.ts +4 -0
- package/dist/cache.d.ts +21 -5
- package/dist/cache.js +178 -25
- package/dist/collection.d.ts +1 -1
- package/dist/collection.js +1 -3
- package/dist/component.d.ts +3 -0
- package/dist/component.js +73 -10
- package/dist/dom.d.ts +35 -24
- package/dist/dom.js +177 -128
- package/dist/effectEvent.d.ts +5 -4
- package/dist/effectEvent.js +29 -38
- 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 +7 -4
- package/dist/index.js +6 -3
- package/dist/infinite-query.d.ts +42 -0
- package/dist/infinite-query.js +150 -0
- package/dist/jsx-runtime.d.ts +6 -0
- package/dist/jsx-runtime.js +14 -1
- package/dist/jsx.d.ts +2 -1
- package/dist/keyed-tasks.d.ts +30 -0
- package/dist/keyed-tasks.js +102 -0
- package/dist/lazy.d.ts +2 -1
- package/dist/lazy.js +20 -7
- package/dist/load.d.ts +1 -1
- package/dist/load.js +1 -1
- package/dist/mount.d.ts +13 -2
- package/dist/mount.js +82 -21
- package/dist/owner.d.ts +9 -5
- package/dist/owner.js +27 -9
- package/dist/pages.d.ts +16 -15
- package/dist/pages.js +8 -5
- package/dist/program.d.ts +14 -2
- package/dist/program.js +96 -36
- package/dist/query-internals.d.ts +5 -3
- package/dist/query.d.ts +7 -1
- package/dist/query.js +7 -3
- package/dist/resource.js +3 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +3 -1
- package/dist/session.js +23 -7
- package/dist/settlement.d.ts +8 -0
- package/dist/settlement.js +32 -0
- package/dist/sharing.js +16 -13
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +31 -25
- package/dist/task.d.ts +1 -1
- package/dist/task.js +3 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +3 -1
- package/dist/testing.d.ts +5 -4
- package/dist/testing.js +4 -2
- package/package.json +9 -1
package/dist/effectEvent.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Fiber from 'effect/Fiber';
|
|
2
4
|
import { reportSafely } from './errors.js';
|
|
3
5
|
import { defaultUiRuntime } from './runtime.js';
|
|
4
6
|
const tag = Symbol('Effect event');
|
|
@@ -6,8 +8,8 @@ export function effectEvent(policy, load, runtime) {
|
|
|
6
8
|
const owner = runtime ?? defaultUiRuntime;
|
|
7
9
|
return (event) => ({ [tag]: true, policy, effect: owner.provide(load(event)) });
|
|
8
10
|
}
|
|
9
|
-
/** Internal listener owner, allocated only when an event returns
|
|
10
|
-
export function eventEffects(report) {
|
|
11
|
+
/** Internal listener owner, allocated only when an event returns an owned request. */
|
|
12
|
+
export function eventEffects(report, settlement) {
|
|
11
13
|
let active;
|
|
12
14
|
let disposed = false;
|
|
13
15
|
const stop = () => {
|
|
@@ -17,46 +19,35 @@ export function eventEffects(report) {
|
|
|
17
19
|
Effect.runFork(Fiber.interrupt(previous.fiber));
|
|
18
20
|
};
|
|
19
21
|
return {
|
|
20
|
-
accept(
|
|
22
|
+
accept(request) {
|
|
21
23
|
if (disposed)
|
|
22
24
|
return;
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
fiber.addObserver((exit) => {
|
|
38
|
-
if (disposed || active !== token)
|
|
39
|
-
return;
|
|
40
|
-
active = undefined;
|
|
41
|
-
if (exit._tag === 'Failure')
|
|
42
|
-
reportSafely(report, exit.cause);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
if (active === token)
|
|
25
|
+
if (request.policy === 'drop' && active)
|
|
26
|
+
return;
|
|
27
|
+
stop();
|
|
28
|
+
const token = {};
|
|
29
|
+
active = token;
|
|
30
|
+
const finished = settlement?.begin();
|
|
31
|
+
try {
|
|
32
|
+
const fiber = Effect.runFork(request.effect);
|
|
33
|
+
token.fiber = fiber;
|
|
34
|
+
fiber.addObserver((exit) => {
|
|
35
|
+
const current = !disposed && active === token;
|
|
36
|
+
if (current)
|
|
47
37
|
active = undefined;
|
|
48
|
-
|
|
38
|
+
if (exit._tag === 'Failure' && (current || !Cause.hasInterruptsOnly(exit.cause)))
|
|
39
|
+
reportSafely(report, exit.cause);
|
|
40
|
+
finished?.();
|
|
41
|
+
});
|
|
42
|
+
if (disposed || active !== token) {
|
|
43
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
49
44
|
}
|
|
50
45
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'then' in value &&
|
|
57
|
-
typeof value.then === 'function') {
|
|
58
|
-
void Promise.resolve(value).catch((error) => reportSafely(report, error));
|
|
59
|
-
reportSafely(report, new Error('Event returned an unowned Promise. Adapt it with fromPromise inside effectEvent or a command.'));
|
|
46
|
+
catch (error) {
|
|
47
|
+
finished?.();
|
|
48
|
+
if (active === token)
|
|
49
|
+
active = undefined;
|
|
50
|
+
reportSafely(report, error);
|
|
60
51
|
}
|
|
61
52
|
},
|
|
62
53
|
dispose() {
|
package/dist/form.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type * as Cause from 'effect/Cause';
|
|
2
|
+
import type * as Effect from 'effect/Effect';
|
|
2
3
|
import { type Transition } from './program.js';
|
|
3
4
|
import type { Snapshot } from './snapshot.js';
|
|
4
5
|
import type { EffectEventRequest } from './effectEvent.js';
|
package/dist/http.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import type * as Schema from 'effect/Schema';
|
|
2
3
|
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
3
4
|
import type * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
|
|
4
5
|
/** Decode successful JSON through a schema, retaining transport, decoding and service types. */
|
package/dist/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
2
|
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
3
3
|
import * as HttpClientResponse from 'effect/unstable/http/HttpClientResponse';
|
|
4
4
|
/** Decode successful JSON through a schema, retaining transport, decoding and service types. */
|
package/dist/index.d.ts
CHANGED
|
@@ -6,12 +6,12 @@ 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
8
|
export { effectEvent } from './effectEvent.js';
|
|
9
|
-
export { mountView, view, ViewBinding, type View } from './dom.js';
|
|
9
|
+
export { mountView, view, list, ViewBinding, type View } from './dom.js';
|
|
10
10
|
export { lazyView, type LazyViewOptions } from './lazy.js';
|
|
11
11
|
export type { JSX } from './jsx.js';
|
|
12
12
|
export { Portal, domBinding, domMount, type DomMount, type PortalProps } from './mount.js';
|
|
13
13
|
export { pages, type PagesMessage, type PagesRequest, type PagesModel, type PagesProgram, type Pagination, } from './pages.js';
|
|
14
|
-
export { program, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
|
|
14
|
+
export { program, mapTransition, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
|
|
15
15
|
export { available, resourceComponent, resourceError, type ResourceMessage, type ResourceModel, } from './resource.js';
|
|
16
16
|
export { fromPromise, type UiLoad, type UiPage } from './load.js';
|
|
17
17
|
export { observeBindings, inspectBindings, mountBindingInspector, type BindingInspector, type BindingInspection, observePrograms, type BindingUpdate, type ProgramUpdate, } from './diagnostics.js';
|
|
@@ -20,7 +20,10 @@ export { slot, type Slot, type CompiledContent } from './dom.js';
|
|
|
20
20
|
export { AsyncContent, type AsyncContentProps } from './AsyncContent.js';
|
|
21
21
|
export { defineTasks, type TaskDefinition, type TasksModel, type TasksMessage, type TaskResults, } from './tasks.js';
|
|
22
22
|
export { uiRuntime, type UiRuntime } from './runtime.js';
|
|
23
|
-
export { query, type Query, type QueryKey } from './query.js';
|
|
24
|
-
export { makeQueryCache, type QueryCache } from './cache.js';
|
|
23
|
+
export { query, queryGroup, type Query, type QueryKey, type QueryGroup } from './query.js';
|
|
24
|
+
export { makeQueryCache, type QueryCache, type QueryCacheOptions } from './cache.js';
|
|
25
25
|
export { queryResource, type QueryResource, observeQuery, lifetime, projectionCache, sessionGroup, type SessionContext, type Read, } from './session.js';
|
|
26
26
|
export { httpJson } from './http.js';
|
|
27
|
+
export { infiniteQuery, infiniteResource, type InfiniteData, type InfiniteQuery, type InfiniteResource, } from './infinite-query.js';
|
|
28
|
+
export { keyedTasks, type TaskHandle, type TaskOutcome } from './keyed-tasks.js';
|
|
29
|
+
export { errorBoundary } from './boundary.js';
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,11 @@ export { patchModel } from './state.js';
|
|
|
5
5
|
export { defineActions } from './actions.js';
|
|
6
6
|
export { taskComponent, taskControls } from './task.js';
|
|
7
7
|
export { effectEvent } from './effectEvent.js';
|
|
8
|
-
export { mountView, view, ViewBinding } from './dom.js';
|
|
8
|
+
export { mountView, view, list, ViewBinding } from './dom.js';
|
|
9
9
|
export { lazyView } from './lazy.js';
|
|
10
10
|
export { Portal, domBinding, domMount } from './mount.js';
|
|
11
11
|
export { pages, } from './pages.js';
|
|
12
|
-
export { program, actionCommand, effectCommand, commandSlot, } from './program.js';
|
|
12
|
+
export { program, mapTransition, actionCommand, effectCommand, commandSlot, } from './program.js';
|
|
13
13
|
export { available, resourceComponent, resourceError, } from './resource.js';
|
|
14
14
|
export { fromPromise } from './load.js';
|
|
15
15
|
export { observeBindings, inspectBindings, mountBindingInspector, observePrograms, } from './diagnostics.js';
|
|
@@ -18,7 +18,10 @@ export { slot } from './dom.js';
|
|
|
18
18
|
export { AsyncContent } from './AsyncContent.js';
|
|
19
19
|
export { defineTasks, } from './tasks.js';
|
|
20
20
|
export { uiRuntime } from './runtime.js';
|
|
21
|
-
export { query } from './query.js';
|
|
21
|
+
export { query, queryGroup } from './query.js';
|
|
22
22
|
export { makeQueryCache } from './cache.js';
|
|
23
23
|
export { queryResource, observeQuery, lifetime, projectionCache, sessionGroup, } from './session.js';
|
|
24
24
|
export { httpJson } from './http.js';
|
|
25
|
+
export { infiniteQuery, infiniteResource, } from './infinite-query.js';
|
|
26
|
+
export { keyedTasks } from './keyed-tasks.js';
|
|
27
|
+
export { errorBoundary } from './boundary.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import { type Query, type QueryArgs, type QueryGroup } from './query.js';
|
|
3
|
+
import type { QueryCache } from './cache.js';
|
|
4
|
+
import { type QueryResource } from './session.js';
|
|
5
|
+
import { type Snapshot } from './snapshot.js';
|
|
6
|
+
export interface InfiniteData<A, Param> {
|
|
7
|
+
readonly pages: readonly {
|
|
8
|
+
readonly param: Param;
|
|
9
|
+
readonly value: A;
|
|
10
|
+
}[];
|
|
11
|
+
readonly next: Param | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface InfiniteQuery<Args, A, Param, E = never, R = never> {
|
|
14
|
+
readonly query: Query<Args, InfiniteData<A, Param>, E, R>;
|
|
15
|
+
readonly page: Query<{
|
|
16
|
+
args: Args;
|
|
17
|
+
param: Param;
|
|
18
|
+
}, A, E, R>;
|
|
19
|
+
readonly maxPages: number;
|
|
20
|
+
readonly next: (value: Snapshot<A>, param: Snapshot<Param>) => Param | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Shared pages with explicit cursor identity. Refresh preserves retained page parameters by default. */
|
|
23
|
+
export declare function infiniteQuery<Args, A, Param, E = never, R = never>(definition: {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly initial: Param;
|
|
26
|
+
readonly load: (args: Snapshot<Args>, param: Snapshot<Param>) => Effect.Effect<A, E, R>;
|
|
27
|
+
readonly next: (value: Snapshot<A>, param: Snapshot<Param>) => Param | undefined;
|
|
28
|
+
readonly maxPages?: number;
|
|
29
|
+
readonly refresh?: 'retained' | 'first';
|
|
30
|
+
readonly groups?: readonly QueryGroup[];
|
|
31
|
+
} & (Args extends QueryArgs<Args> ? unknown : {
|
|
32
|
+
readonly nonSerializableQueryArguments: never;
|
|
33
|
+
}) & (Param extends QueryArgs<Param> ? unknown : {
|
|
34
|
+
readonly nonSerializablePageParameters: never;
|
|
35
|
+
})): InfiniteQuery<Args, A, Param, E, R>;
|
|
36
|
+
export interface InfiniteResource<Args, A, Param, E = never> extends QueryResource<Args, InfiniteData<A, Param>, E> {
|
|
37
|
+
loadNext(): Effect.Effect<Snapshot<InfiniteData<A, Param>>, E>;
|
|
38
|
+
retryPage(param: Param | Snapshot<Param>): Effect.Effect<Snapshot<InfiniteData<A, Param>>, E>;
|
|
39
|
+
seed(args: Args | Snapshot<Args>, data: InfiniteData<A, Param> | Snapshot<InfiniteData<A, Param>>): Snapshot<InfiniteData<A, Param>>;
|
|
40
|
+
}
|
|
41
|
+
/** Cache-owned results, observer-owned subscriptions. Operations return typed Effects for task composition. */
|
|
42
|
+
export declare function infiniteResource<Args, A, Param, E, R>(cache: QueryCache<R>, definition: InfiniteQuery<Args, A, Param, E, NoInfer<R>>): InfiniteResource<Args, A, Param, E>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import { query, encodeQueryKey, } from './query.js';
|
|
3
|
+
import { cacheInternals } from './cache-internals.js';
|
|
4
|
+
import { queryResource } from './session.js';
|
|
5
|
+
import { protectSnapshot } from './snapshot.js';
|
|
6
|
+
/** Shared pages with explicit cursor identity. Refresh preserves retained page parameters by default. */
|
|
7
|
+
export function infiniteQuery(definition) {
|
|
8
|
+
const config = Object.freeze({ ...definition });
|
|
9
|
+
const maxPages = config.maxPages ?? Infinity;
|
|
10
|
+
if (maxPages !== Infinity && (!Number.isInteger(maxPages) || maxPages < 1))
|
|
11
|
+
throw new RangeError('maxPages must be a positive integer.');
|
|
12
|
+
encodeQueryKey(config.initial);
|
|
13
|
+
const initial = protectSnapshot(config.initial);
|
|
14
|
+
const next = (value, param) => {
|
|
15
|
+
const result = config.next(value, param);
|
|
16
|
+
encodeQueryKey(result);
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
const page = query({
|
|
20
|
+
name: `${config.name}:page`,
|
|
21
|
+
load: ({ args, param }) => config.load(args, param),
|
|
22
|
+
});
|
|
23
|
+
const root = query({
|
|
24
|
+
name: config.name,
|
|
25
|
+
...(config.groups ? { groups: config.groups } : {}),
|
|
26
|
+
load: (args, previous) => {
|
|
27
|
+
const params = config.refresh !== 'first' && previous?.pages.length
|
|
28
|
+
? previous.pages.map((page) => page.param)
|
|
29
|
+
: [initial];
|
|
30
|
+
return Effect.forEach(params, (param) => config.load(args, param).pipe(Effect.map((value) => ({ param: param, value })))).pipe(Effect.map((pages) => {
|
|
31
|
+
const last = pages[pages.length - 1];
|
|
32
|
+
return {
|
|
33
|
+
pages,
|
|
34
|
+
next: next(protectSnapshot(last.value), last.param),
|
|
35
|
+
};
|
|
36
|
+
}));
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return Object.freeze({ query: root, page, maxPages, next });
|
|
40
|
+
}
|
|
41
|
+
const operations = new WeakMap();
|
|
42
|
+
function operationBook(cache, definition) {
|
|
43
|
+
let definitions = operations.get(cache);
|
|
44
|
+
if (!definitions) {
|
|
45
|
+
definitions = new WeakMap();
|
|
46
|
+
operations.set(cache, definitions);
|
|
47
|
+
}
|
|
48
|
+
let book = definitions.get(definition);
|
|
49
|
+
if (!book) {
|
|
50
|
+
book = new Map();
|
|
51
|
+
definitions.set(definition, book);
|
|
52
|
+
}
|
|
53
|
+
return book;
|
|
54
|
+
}
|
|
55
|
+
/** Cache-owned results, observer-owned subscriptions. Operations return typed Effects for task composition. */
|
|
56
|
+
export function infiniteResource(cache, definition) {
|
|
57
|
+
const resource = queryResource({ cache }, definition.query);
|
|
58
|
+
const internal = cacheInternals(cache);
|
|
59
|
+
let selected;
|
|
60
|
+
let disposed = false;
|
|
61
|
+
const stopReset = cache.onReset(() => {
|
|
62
|
+
selected = undefined;
|
|
63
|
+
});
|
|
64
|
+
const book = operationBook(cache, definition.query);
|
|
65
|
+
const updatePage = (retry) => Effect.gen(function* () {
|
|
66
|
+
if (disposed || selected === undefined)
|
|
67
|
+
return yield* Effect.interrupt;
|
|
68
|
+
const args = selected;
|
|
69
|
+
const current = yield* cache.prefetch(definition.query, args);
|
|
70
|
+
const param = retry ? retry.param : current.next;
|
|
71
|
+
if (param === undefined)
|
|
72
|
+
return current;
|
|
73
|
+
const pageKey = encodeQueryKey(param);
|
|
74
|
+
const retained = current.pages.some((page) => encodeQueryKey(page.param) === pageKey);
|
|
75
|
+
if (retry && !retained && encodeQueryKey(current.next) !== pageKey)
|
|
76
|
+
return yield* Effect.die(new RangeError('Retry a retained page or the next page parameter.'));
|
|
77
|
+
const key = encodeQueryKey(args);
|
|
78
|
+
const revision = internal.revision(definition.query, args);
|
|
79
|
+
let operation = book.get(key);
|
|
80
|
+
if (!operation || operation.revision !== revision) {
|
|
81
|
+
operation = { revision, active: 0 };
|
|
82
|
+
book.set(key, operation);
|
|
83
|
+
}
|
|
84
|
+
const state = operation;
|
|
85
|
+
state.active++;
|
|
86
|
+
return yield* Effect.gen(function* () {
|
|
87
|
+
const pageArgs = { args, param };
|
|
88
|
+
const value = yield* cache.prefetch(definition.page, pageArgs, { refresh: true });
|
|
89
|
+
let result;
|
|
90
|
+
cache.batch(() => {
|
|
91
|
+
const latest = cache.getQueryData(definition.query, args);
|
|
92
|
+
result = latest;
|
|
93
|
+
// Other page operations can merge. External refresh, writes and reset own a new revision.
|
|
94
|
+
if (!latest ||
|
|
95
|
+
book.get(key) !== state ||
|
|
96
|
+
state.revision !== internal.revision(definition.query, args))
|
|
97
|
+
return;
|
|
98
|
+
const index = latest.pages.findIndex((page) => encodeQueryKey(page.param) === pageKey);
|
|
99
|
+
if (index < 0 && (retained || encodeQueryKey(latest.next) !== pageKey))
|
|
100
|
+
return;
|
|
101
|
+
if (index >= 0 && latest.pages[index].value === value)
|
|
102
|
+
return;
|
|
103
|
+
const pages = [...latest.pages];
|
|
104
|
+
const page = { param, value };
|
|
105
|
+
if (index >= 0)
|
|
106
|
+
pages[index] = page;
|
|
107
|
+
else
|
|
108
|
+
pages.push(page);
|
|
109
|
+
const kept = pages.slice(-definition.maxPages);
|
|
110
|
+
const last = kept[kept.length - 1];
|
|
111
|
+
result = cache.setQueryData(definition.query, args, {
|
|
112
|
+
pages: kept,
|
|
113
|
+
next: definition.next(last.value, last.param),
|
|
114
|
+
});
|
|
115
|
+
state.revision = internal.revision(definition.query, args);
|
|
116
|
+
});
|
|
117
|
+
return result ?? (yield* Effect.interrupt);
|
|
118
|
+
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
119
|
+
state.active--;
|
|
120
|
+
if (state.active === 0 && book.get(key) === state)
|
|
121
|
+
book.delete(key);
|
|
122
|
+
})));
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
read: resource.read,
|
|
126
|
+
subscribe: resource.subscribe,
|
|
127
|
+
select(args) {
|
|
128
|
+
selected = args;
|
|
129
|
+
resource.select(args);
|
|
130
|
+
},
|
|
131
|
+
refresh: resource.refresh,
|
|
132
|
+
loadNext: () => updatePage(),
|
|
133
|
+
retryPage: (param) => updatePage({ param }),
|
|
134
|
+
seed(args, data) {
|
|
135
|
+
if (!data.pages.length || data.pages.length > definition.maxPages)
|
|
136
|
+
throw new RangeError('Seed pages must fit the retained page range.');
|
|
137
|
+
encodeQueryKey(data.next);
|
|
138
|
+
const keys = data.pages.map((page) => encodeQueryKey(page.param));
|
|
139
|
+
if (new Set(keys).size !== keys.length)
|
|
140
|
+
throw new TypeError('Seed page parameters must be unique.');
|
|
141
|
+
return cache.setQueryData(definition.query, args, data);
|
|
142
|
+
},
|
|
143
|
+
dispose() {
|
|
144
|
+
disposed = true;
|
|
145
|
+
selected = undefined;
|
|
146
|
+
stopReset();
|
|
147
|
+
resource.dispose();
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/dist/jsx-runtime.d.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
1
|
export type { JSX } from './jsx.js';
|
|
2
|
+
import type { JSX } from './jsx.js';
|
|
3
|
+
export declare function Fragment(props: {
|
|
4
|
+
readonly children?: JSX.Element;
|
|
5
|
+
}): JSX.Element;
|
|
6
|
+
export declare function jsx(type: string | ((props: never) => JSX.Element), props: Readonly<Record<string, unknown>>): JSX.Element;
|
|
7
|
+
export { jsx as jsxs, jsx as jsxDEV };
|
package/dist/jsx-runtime.js
CHANGED
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { markup } from './dom.js';
|
|
2
|
+
export function Fragment(props) {
|
|
3
|
+
return props.children;
|
|
4
|
+
}
|
|
5
|
+
const tags = new Map();
|
|
6
|
+
export function jsx(type, props) {
|
|
7
|
+
if (typeof type !== 'string')
|
|
8
|
+
return type(props);
|
|
9
|
+
let render = tags.get(type);
|
|
10
|
+
if (!render)
|
|
11
|
+
tags.set(type, (render = markup(type)));
|
|
12
|
+
return render(props);
|
|
13
|
+
}
|
|
14
|
+
export { jsx as jsxs, jsx as jsxDEV };
|
package/dist/jsx.d.ts
CHANGED
|
@@ -16,10 +16,11 @@ export declare namespace JSX {
|
|
|
16
16
|
export interface ElementArray extends ReadonlyArray<Element>, SnapshotOpaque {
|
|
17
17
|
}
|
|
18
18
|
export type Element = string | number | boolean | null | undefined | CompiledContent | ElementArray;
|
|
19
|
+
export type EventResult = void | boolean | EffectEventRequest;
|
|
19
20
|
export type EventHandler<T, E extends Event> = (event: E & {
|
|
20
21
|
currentTarget: T;
|
|
21
22
|
target: EventTarget & globalThis.Element;
|
|
22
|
-
}) =>
|
|
23
|
+
}) => EventResult;
|
|
23
24
|
export type EventHandlerUnion<T, E extends Event> = EventHandler<T, E>;
|
|
24
25
|
export type CSSProperties = {
|
|
25
26
|
[property: string]: string | number | undefined | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import { type TaskPolicy } from './program.js';
|
|
4
|
+
import type { DisposableOwner } from './owner.js';
|
|
5
|
+
import { type UiRuntime } from './runtime.js';
|
|
6
|
+
import { type Snapshot } from './snapshot.js';
|
|
7
|
+
export type TaskOutcome<A, E> = {
|
|
8
|
+
readonly _tag: 'Success';
|
|
9
|
+
readonly value: Snapshot<A>;
|
|
10
|
+
} | {
|
|
11
|
+
readonly _tag: 'Failure';
|
|
12
|
+
readonly cause: Cause.Cause<E>;
|
|
13
|
+
} | {
|
|
14
|
+
readonly _tag: 'Cancelled' | 'Superseded' | 'Dropped';
|
|
15
|
+
};
|
|
16
|
+
export interface TaskHandle<A, E> {
|
|
17
|
+
readonly outcome: Effect.Effect<TaskOutcome<A, E>>;
|
|
18
|
+
}
|
|
19
|
+
/** Per-entity serialization. Inputs are captured now; run reads services when work starts. */
|
|
20
|
+
export declare function keyedTasks<Key, Input, A, E = never, R = never>(owner: DisposableOwner, definition: {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly policy: TaskPolicy;
|
|
23
|
+
readonly run: (key: Key, input: Snapshot<Input>) => Effect.Effect<A, E, R>;
|
|
24
|
+
}, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): {
|
|
25
|
+
submit(key: Key, input: Input | Snapshot<Input>): TaskHandle<A, E>;
|
|
26
|
+
cancel(key: Key): void;
|
|
27
|
+
drain(...selected: [] | [Key]): Effect.Effect<void>;
|
|
28
|
+
close: () => Effect.Effect<void, never, never>;
|
|
29
|
+
dispose: () => void;
|
|
30
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Deferred from 'effect/Deferred';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Exit from 'effect/Exit';
|
|
5
|
+
import { commandSlot, program } from './program.js';
|
|
6
|
+
import { defaultUiRuntime } from './runtime.js';
|
|
7
|
+
import { reportError, reportSafely } from './errors.js';
|
|
8
|
+
import { protectSnapshot } from './snapshot.js';
|
|
9
|
+
/** Per-entity serialization. Inputs are captured now; run reads services when work starts. */
|
|
10
|
+
export function keyedTasks(owner, definition, ...provided) {
|
|
11
|
+
const runtime = provided[0] ?? defaultUiRuntime;
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
let disposed = false;
|
|
14
|
+
const slot = commandSlot(definition.name);
|
|
15
|
+
const makeSource = () => program({
|
|
16
|
+
initial: null,
|
|
17
|
+
update: (model, message) => 'command' in message ? { model, commands: [message.command] } : { model, cancel: [slot] },
|
|
18
|
+
});
|
|
19
|
+
// Each key owns a program, including its interrupted fibers, so drain never waits on other keys.
|
|
20
|
+
const get = (key) => {
|
|
21
|
+
let entry = entries.get(key);
|
|
22
|
+
if (!entry) {
|
|
23
|
+
entry = { source: makeSource(), version: 0, retiring: false };
|
|
24
|
+
entries.set(key, entry);
|
|
25
|
+
}
|
|
26
|
+
return entry;
|
|
27
|
+
};
|
|
28
|
+
const dispose = () => {
|
|
29
|
+
disposed = true;
|
|
30
|
+
for (const entry of entries.values())
|
|
31
|
+
entry.source.dispose();
|
|
32
|
+
};
|
|
33
|
+
const close = () => Effect.gen(function* () {
|
|
34
|
+
dispose();
|
|
35
|
+
yield* Effect.all([...entries.values()].map((entry) => entry.source.close()), { concurrency: 'unbounded' });
|
|
36
|
+
entries.clear();
|
|
37
|
+
});
|
|
38
|
+
owner.own({ dispose, close });
|
|
39
|
+
return {
|
|
40
|
+
submit(key, input) {
|
|
41
|
+
if (disposed || owner.disposed)
|
|
42
|
+
return { outcome: Effect.succeed(Object.freeze({ _tag: 'Cancelled' })) };
|
|
43
|
+
const captured = protectSnapshot(input);
|
|
44
|
+
const entry = get(key);
|
|
45
|
+
entry.version++;
|
|
46
|
+
const result = Deferred.makeUnsafe();
|
|
47
|
+
const outcome = Deferred.await(result);
|
|
48
|
+
const settle = (value) => {
|
|
49
|
+
Deferred.doneUnsafe(result, Effect.succeed(protectSnapshot(value)));
|
|
50
|
+
};
|
|
51
|
+
Effect.runFork(Effect.gen(function* () {
|
|
52
|
+
yield* outcome;
|
|
53
|
+
if (entry.retiring)
|
|
54
|
+
return;
|
|
55
|
+
entry.retiring = true;
|
|
56
|
+
let version;
|
|
57
|
+
do {
|
|
58
|
+
version = entry.version;
|
|
59
|
+
yield* entry.source.awaitStopped();
|
|
60
|
+
} while (version !== entry.version);
|
|
61
|
+
if (entries.get(key) === entry) {
|
|
62
|
+
entries.delete(key);
|
|
63
|
+
entry.source.dispose();
|
|
64
|
+
}
|
|
65
|
+
})).addObserver((exit) => {
|
|
66
|
+
if (Exit.isFailure(exit))
|
|
67
|
+
reportSafely(reportError, exit.cause);
|
|
68
|
+
});
|
|
69
|
+
const action = Effect.uninterruptibleMask((restore) => restore(Effect.suspend(() => definition.run(key, captured)).pipe(Effect.map((value) => protectSnapshot(value)))).pipe(Effect.exit, Effect.flatMap((exit) => Effect.sync(() => settle(Exit.isSuccess(exit)
|
|
70
|
+
? { _tag: 'Success', value: exit.value }
|
|
71
|
+
: Cause.hasInterruptsOnly(exit.cause)
|
|
72
|
+
? { _tag: 'Cancelled' }
|
|
73
|
+
: { _tag: 'Failure', cause: exit.cause })))));
|
|
74
|
+
entry.source.send({
|
|
75
|
+
command: {
|
|
76
|
+
slot,
|
|
77
|
+
policy: definition.policy,
|
|
78
|
+
action: runtime.provide(action),
|
|
79
|
+
onDiscard: (_tag) => settle({ _tag }),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return { outcome };
|
|
83
|
+
},
|
|
84
|
+
cancel(key) {
|
|
85
|
+
const entry = entries.get(key);
|
|
86
|
+
if (entry) {
|
|
87
|
+
entry.version++;
|
|
88
|
+
entry.source.send({ cancel: true });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
drain(...selected) {
|
|
92
|
+
return Effect.suspend(() => {
|
|
93
|
+
const pending = selected.length
|
|
94
|
+
? [entries.get(selected[0])].filter((entry) => !!entry)
|
|
95
|
+
: [...entries.values()];
|
|
96
|
+
return Effect.all(pending.map((entry) => Effect.andThen(entry.source.awaitIdle(), entry.source.awaitStopped())), { concurrency: 'unbounded', discard: true });
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
close,
|
|
100
|
+
dispose,
|
|
101
|
+
};
|
|
102
|
+
}
|
package/dist/lazy.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
2
3
|
import { type View } from './dom.js';
|
|
3
4
|
import { type UiRuntime } from './runtime.js';
|
|
4
5
|
export interface LazyViewOptions<Model, Message, E> {
|
package/dist/lazy.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Fiber from 'effect/Fiber';
|
|
2
4
|
import { child, compiled, viewRegion } from './dom.js';
|
|
3
5
|
import { reportSafely } from './errors.js';
|
|
4
6
|
import { defaultUiRuntime } from './runtime.js';
|
|
@@ -25,15 +27,21 @@ export function lazyView(load, ...provided) {
|
|
|
25
27
|
scope.cleanups.push(interrupt);
|
|
26
28
|
if (scope.disposed)
|
|
27
29
|
return;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const finished = scope.settlement.begin();
|
|
31
|
+
try {
|
|
32
|
+
fiber = Effect.runFork(runtime.provide(Effect.suspend(load)));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
finished();
|
|
36
|
+
throw error;
|
|
32
37
|
}
|
|
33
38
|
fiber.addObserver((exit) => {
|
|
34
|
-
if (scope.disposed)
|
|
35
|
-
return;
|
|
36
39
|
try {
|
|
40
|
+
if (scope.disposed) {
|
|
41
|
+
if (exit._tag === 'Failure' && !Cause.hasInterruptsOnly(exit.cause))
|
|
42
|
+
reportSafely(scope.report, exit.cause);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
37
45
|
if (exit._tag === 'Success') {
|
|
38
46
|
loaded = exit.value;
|
|
39
47
|
render(loaded);
|
|
@@ -53,6 +61,11 @@ export function lazyView(load, ...provided) {
|
|
|
53
61
|
catch (error) {
|
|
54
62
|
reportSafely(scope.report, error);
|
|
55
63
|
}
|
|
64
|
+
finally {
|
|
65
|
+
finished();
|
|
66
|
+
}
|
|
56
67
|
});
|
|
68
|
+
if (scope.disposed)
|
|
69
|
+
interrupt();
|
|
57
70
|
});
|
|
58
71
|
}
|
package/dist/load.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
2
|
export type UiLoad<A, E = unknown, R = never> = Effect.Effect<A, E, R>;
|
|
3
3
|
export declare const loadEffect: <A, E, R>(load: () => UiLoad<A, E, R>) => UiLoad<A, E, R>;
|
|
4
4
|
/** Adapt Promise libraries at the integration boundary. Pass the signal to APIs that support it. */
|
package/dist/load.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
2
|
export const loadEffect = (load) => Effect.suspend(load);
|
|
3
3
|
/** Adapt Promise libraries at the integration boundary. Pass the signal to APIs that support it. */
|
|
4
4
|
export const fromPromise = (load) => Effect.tryPromise({ try: load, catch: (error) => error });
|
package/dist/mount.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
import { type UiRuntime } from './runtime.js';
|
|
5
5
|
export { Portal, type PortalProps } from './dom.js';
|
|
6
6
|
type Work<R = never> = (() => void) | {
|
|
@@ -16,7 +16,18 @@ export interface DomMount<T extends Element = Element> {
|
|
|
16
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>;
|
|
17
17
|
/** A stable DOM lifecycle with fresh immutable inputs, for focus, measurements and native events. */
|
|
18
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
|
+
/** Allocate the lifetime before acquisition so reentrant publications can update or close it. */
|
|
20
|
+
export declare function prepareMount<T extends Element>(element: T, mount: DomMount<T>, report?: ReportError): {
|
|
21
|
+
closed: Effect.Effect<void, never, never>;
|
|
22
|
+
start(): void;
|
|
23
|
+
close: () => Effect.Effect<void, never, never>;
|
|
24
|
+
update(next: DomMount<T>): boolean;
|
|
25
|
+
dispose: () => void;
|
|
26
|
+
};
|
|
19
27
|
export declare function startMount<T extends Element>(element: T, mount: DomMount<T>, report?: ReportError): {
|
|
28
|
+
closed: Effect.Effect<void, never, never>;
|
|
29
|
+
start(): void;
|
|
30
|
+
close: () => Effect.Effect<void, never, never>;
|
|
20
31
|
update(next: DomMount<T>): boolean;
|
|
21
|
-
dispose()
|
|
32
|
+
dispose: () => void;
|
|
22
33
|
};
|