effectweb 0.3.0 → 0.3.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/AsyncContent.js +1 -1
- 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 +20 -4
- package/dist/cache.js +174 -25
- package/dist/component.d.ts +3 -0
- package/dist/component.js +70 -10
- package/dist/dom.d.ts +12 -4
- package/dist/dom.js +38 -20
- package/dist/effectEvent.d.ts +2 -1
- package/dist/effectEvent.js +4 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/infinite-query.d.ts +42 -0
- package/dist/infinite-query.js +150 -0
- package/dist/intrinsic.d.ts +2 -0
- package/dist/intrinsic.js +120 -0
- package/dist/keyed-tasks.d.ts +29 -0
- package/dist/keyed-tasks.js +97 -0
- package/dist/lazy.js +9 -1
- package/dist/mount.d.ts +3 -1
- package/dist/mount.js +29 -11
- package/dist/owner.d.ts +3 -1
- package/dist/owner.js +39 -3
- package/dist/pages.d.ts +15 -14
- package/dist/pages.js +6 -4
- package/dist/program.d.ts +10 -0
- package/dist/program.js +71 -24
- package/dist/query-internals.d.ts +4 -2
- package/dist/query.d.ts +6 -0
- package/dist/query.js +7 -1
- package/dist/session.js +13 -4
- package/dist/settlement.d.ts +7 -0
- package/dist/settlement.js +27 -0
- package/dist/sharing.js +16 -13
- package/dist/slotIdentity.d.ts +1 -0
- package/dist/slotIdentity.js +2 -0
- package/dist/snapshot.js +32 -24
- package/dist/tasks.js +1 -0
- package/package.json +9 -1
package/dist/AsyncContent.js
CHANGED
|
@@ -50,7 +50,7 @@ const implementation = component({
|
|
|
50
50
|
view: /* @__PURE__ */ _ew_dom.compiled((_ew_scope_1, _ew_parent_2, _ew_before_3) => {
|
|
51
51
|
const _ew_derived_4 = _ew_scope_1.derive(() => [_ew_scope_1.value?.props], () => {
|
|
52
52
|
const _ew_capture_6 = _ew_scope_1.value;
|
|
53
|
-
return _ew_capture_6.props;
|
|
53
|
+
return _ew_dom.intrinsic(_ew_dom.intrinsic(_ew_capture_6, ["props", "content"], "@slot"), ["props", "failure"], "@slot").props;
|
|
54
54
|
});
|
|
55
55
|
const _ew_derived_7 = _ew_scope_1.derive(() => [_ew_derived_4()?.result], () => {
|
|
56
56
|
const _ew_capture_9 = _ew_derived_4();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type View } from './dom.js';
|
|
2
|
+
import { type ReportError } from './errors.js';
|
|
3
|
+
import { type Snapshot } from './snapshot.js';
|
|
4
|
+
export declare function errorBoundary<Model, Message>(content: View<Model, Message>, options: {
|
|
5
|
+
readonly fallback: View<{
|
|
6
|
+
readonly model: NoInfer<Model>;
|
|
7
|
+
readonly error: unknown;
|
|
8
|
+
}, NoInfer<Message>>;
|
|
9
|
+
readonly reset?: (model: Snapshot<NoInfer<Model>>) => unknown;
|
|
10
|
+
readonly onError?: ReportError;
|
|
11
|
+
}): View<Model, Message>;
|
package/dist/boundary.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { child, compiled, viewRegion } from './dom.js';
|
|
2
|
+
import { reportError, reportSafely } from './errors.js';
|
|
3
|
+
import { protectSnapshot } from './snapshot.js';
|
|
4
|
+
export function errorBoundary(content, options) {
|
|
5
|
+
return compiled((scope, parent, before) => {
|
|
6
|
+
let failed = false;
|
|
7
|
+
let error;
|
|
8
|
+
let generation = 0;
|
|
9
|
+
let reset = options.reset?.(scope.value);
|
|
10
|
+
const fallback = compiled((scope, parent, before) => {
|
|
11
|
+
child(scope, parent, before, options.fallback, () => [scope.value], () => protectSnapshot({ model: scope.value, error }), scope.send);
|
|
12
|
+
});
|
|
13
|
+
const region = { manual: true, report: scope.report };
|
|
14
|
+
const render = viewRegion(scope, parent, before, region);
|
|
15
|
+
function showFallback() {
|
|
16
|
+
if (scope.disposed || !failed)
|
|
17
|
+
return;
|
|
18
|
+
region.report = scope.report;
|
|
19
|
+
try {
|
|
20
|
+
render(fallback);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
reportSafely(scope.report, error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function fail(cause, attempt) {
|
|
27
|
+
if (scope.disposed || attempt !== generation) {
|
|
28
|
+
reportSafely(options.onError ?? reportError, cause);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
failed = true;
|
|
32
|
+
error = cause;
|
|
33
|
+
const token = ++generation;
|
|
34
|
+
render(undefined);
|
|
35
|
+
reportSafely(options.onError ?? reportError, cause);
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
if (token === generation)
|
|
38
|
+
showFallback();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const update = () => {
|
|
42
|
+
const next = options.reset?.(scope.value);
|
|
43
|
+
if (!Object.is(reset, next)) {
|
|
44
|
+
reset = next;
|
|
45
|
+
generation++;
|
|
46
|
+
render(undefined);
|
|
47
|
+
failed = false;
|
|
48
|
+
error = undefined;
|
|
49
|
+
}
|
|
50
|
+
if (failed)
|
|
51
|
+
showFallback();
|
|
52
|
+
else {
|
|
53
|
+
const attempt = generation;
|
|
54
|
+
region.report = (cause) => fail(cause, attempt);
|
|
55
|
+
try {
|
|
56
|
+
render(content);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
fail(error, attempt);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
scope.jobs.push(update);
|
|
64
|
+
update();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -5,6 +5,10 @@ import type { QueryCache } from './cache.js';
|
|
|
5
5
|
import type { Query } from './query.js';
|
|
6
6
|
import type { Snapshot } from './snapshot.js';
|
|
7
7
|
export interface CacheInternals<R> {
|
|
8
|
+
refresh(atom: Atom.Atom<unknown>): void;
|
|
9
|
+
readonly disposed: () => boolean;
|
|
10
|
+
revision<Args, A, E>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): number | undefined;
|
|
11
|
+
retain(atom: Atom.Atom<unknown>): () => void;
|
|
8
12
|
readonly registry: AtomRegistry.AtomRegistry;
|
|
9
13
|
readonly generation: Atom.Writable<number>;
|
|
10
14
|
query<Args, A, E>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): Atom.Atom<AsyncResult.AsyncResult<Snapshot<A>, E>>;
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { type Snapshot } from './snapshot.js';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
|
-
import { type Query } from './query.js';
|
|
3
|
+
import { type Query, type QueryGroup } from './query.js';
|
|
4
4
|
import type { UiRuntime } from './runtime.js';
|
|
5
5
|
export { loadEffect, type UiLoad } from './load.js';
|
|
6
6
|
export { shareValue } from './share.js';
|
|
7
|
-
export
|
|
8
|
-
|
|
7
|
+
export interface QueryCacheOptions {
|
|
8
|
+
readonly retention?: number;
|
|
9
|
+
readonly unused?: 'retain' | 'cancel' | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function makeQueryCache(options?: QueryCacheOptions): QueryCache<never>;
|
|
12
|
+
export declare function makeQueryCache<R>(runtime: UiRuntime<R>, options?: QueryCacheOptions): QueryCache<R>;
|
|
9
13
|
/** A cache owns one registry and the query resources published through it. */
|
|
10
14
|
export interface QueryCache<R = never> {
|
|
11
|
-
|
|
15
|
+
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;
|
|
18
|
+
invalidateGroup(group: QueryGroup): void;
|
|
19
|
+
/** 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;
|
|
21
|
+
/** 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?: {
|
|
24
|
+
readonly refresh?: boolean;
|
|
25
|
+
}): Effect.Effect<Snapshot<A>, E>;
|
|
12
26
|
/** Publish a protected success, including undefined, and supersede any pending load for this key. */
|
|
13
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>;
|
|
14
28
|
/**
|
|
@@ -21,5 +35,7 @@ export interface QueryCache<R = never> {
|
|
|
21
35
|
/** Observe account resets without exposing registry mutation. */
|
|
22
36
|
onReset(listener: () => void): () => void;
|
|
23
37
|
resetResources(): void;
|
|
38
|
+
/** Interrupt every request and wait for its finalizers, including previously canceled requests. */
|
|
39
|
+
close(): Promise<void>;
|
|
24
40
|
dispose(): void;
|
|
25
41
|
}
|
package/dist/cache.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Settlement } from './settlement.js';
|
|
1
2
|
import { protectSnapshot } from './snapshot.js';
|
|
2
|
-
import { Effect, Option } from 'effect';
|
|
3
|
+
import { Deferred, Effect, Option } from 'effect';
|
|
3
4
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
4
5
|
import * as Atom from 'effect/unstable/reactivity/Atom';
|
|
5
6
|
import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
|
|
@@ -10,18 +11,29 @@ import { registerCache } from './cache-internals.js';
|
|
|
10
11
|
import { queryDefinition } from './query-internals.js';
|
|
11
12
|
export { loadEffect } from './load.js';
|
|
12
13
|
export { shareValue } from './share.js';
|
|
13
|
-
export function makeQueryCache(
|
|
14
|
-
return
|
|
14
|
+
export function makeQueryCache(runtimeOrOptions, options = {}) {
|
|
15
|
+
return runtimeOrOptions && 'provide' in runtimeOrOptions
|
|
16
|
+
? createQueryCache(runtimeOrOptions, options)
|
|
17
|
+
: createQueryCache(undefined, runtimeOrOptions);
|
|
15
18
|
}
|
|
16
|
-
function createQueryCache(runtime) {
|
|
17
|
-
const
|
|
19
|
+
function createQueryCache(runtime, options = {}) {
|
|
20
|
+
const retention = options.retention ?? 30_000;
|
|
21
|
+
if (!Number.isFinite(retention) || retention < 0)
|
|
22
|
+
throw new RangeError('Query retention must be finite and nonnegative.');
|
|
23
|
+
const registry = AtomRegistry.make({ defaultIdleTTL: retention });
|
|
24
|
+
const cancelValue = Symbol('cancel');
|
|
25
|
+
const removeValue = Symbol('remove');
|
|
18
26
|
const generation = Atom.keepAlive(Atom.make(0));
|
|
19
27
|
const resources = new Map();
|
|
28
|
+
const entriesByAtom = new WeakMap();
|
|
20
29
|
// Follow registry-node lifetime; retaining definitions must not retain evicted data.
|
|
21
30
|
const values = new WeakMap();
|
|
22
31
|
const identities = new WeakMap();
|
|
23
32
|
let disposed = false;
|
|
33
|
+
const settlement = new Settlement();
|
|
34
|
+
let closing;
|
|
24
35
|
let nextId = 0;
|
|
36
|
+
let nextRevision = 0;
|
|
25
37
|
const identity = (definition) => {
|
|
26
38
|
let id = identities.get(definition);
|
|
27
39
|
if (id === undefined) {
|
|
@@ -37,28 +49,54 @@ function createQueryCache(runtime) {
|
|
|
37
49
|
else {
|
|
38
50
|
const loaded = Atom.make((get) => {
|
|
39
51
|
const previous = Option.flatMap(get.self(), AsyncResult.value);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
next.revision = ++nextRevision;
|
|
53
|
+
next.canceled = false;
|
|
54
|
+
const revision = next.revision;
|
|
55
|
+
return Effect.suspend(() => {
|
|
56
|
+
const finish = settlement.begin();
|
|
57
|
+
return loadEffect(() => next.load()).pipe(Effect.map((value) => {
|
|
58
|
+
const shared = Option.isSome(previous)
|
|
59
|
+
? share(previous.value, value)
|
|
60
|
+
: value;
|
|
61
|
+
const snapshot = protectSnapshot(shared);
|
|
62
|
+
if (!disposed && revision === next.revision)
|
|
63
|
+
remember(next, snapshot);
|
|
64
|
+
return snapshot;
|
|
65
|
+
}), Effect.ensuring(Effect.sync(finish)));
|
|
66
|
+
});
|
|
48
67
|
});
|
|
49
68
|
const next = {
|
|
50
69
|
load,
|
|
70
|
+
users: 0,
|
|
71
|
+
prefetches: new Set(),
|
|
72
|
+
revision: 0,
|
|
51
73
|
atom: Atom.writable(loaded.read, (context, value) => {
|
|
52
74
|
Atom.batch(() => {
|
|
75
|
+
next.revision = ++nextRevision;
|
|
53
76
|
// Refresh disposes the load lifetime; setSelf publishes without starting another load.
|
|
54
77
|
context.refreshSelf();
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
if (value === cancelValue || value === removeValue) {
|
|
79
|
+
const previous = value === removeValue ? undefined : previousValue(next);
|
|
80
|
+
next.canceled = true;
|
|
81
|
+
if (value === removeValue) {
|
|
82
|
+
delete next.loadedAt;
|
|
83
|
+
const node = registry.getNodes().get(next.atom);
|
|
84
|
+
if (node)
|
|
85
|
+
values.delete(node);
|
|
86
|
+
}
|
|
87
|
+
context.setSelf(previous ? AsyncResult.success(previous.value) : AsyncResult.initial());
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
next.canceled = false;
|
|
91
|
+
remember(next, value);
|
|
92
|
+
context.setSelf(AsyncResult.success(value));
|
|
93
|
+
}
|
|
57
94
|
});
|
|
58
95
|
}),
|
|
59
96
|
};
|
|
60
97
|
entry = next;
|
|
61
98
|
resources.set(key, entry);
|
|
99
|
+
entriesByAtom.set(entry.atom, entry);
|
|
62
100
|
}
|
|
63
101
|
// Bound definitions as well as the registry's values. Mounted resources stay shared.
|
|
64
102
|
if (resources.size > 512) {
|
|
@@ -90,26 +128,117 @@ function createQueryCache(runtime) {
|
|
|
90
128
|
const config = queryDefinition(definition);
|
|
91
129
|
protectSnapshot(args);
|
|
92
130
|
const { entry, atom } = acquire(queryKey(definition, args), () => {
|
|
93
|
-
const effect = Effect.suspend(() => config.load(args));
|
|
131
|
+
const effect = Effect.suspend(() => config.load(args, previousValue(resources.get(queryKey(definition, args)))?.value));
|
|
94
132
|
return runtime ? runtime.provide(effect) : effect;
|
|
95
133
|
}, config.share);
|
|
96
134
|
entry.query = definition;
|
|
135
|
+
entry.args = args;
|
|
136
|
+
entry.groups = config.groups;
|
|
137
|
+
entry.unused = config.unused ?? options.unused;
|
|
97
138
|
return { entry, atom, config };
|
|
98
139
|
};
|
|
140
|
+
const refresh = (entry) => {
|
|
141
|
+
entry.revision = ++nextRevision;
|
|
142
|
+
registry.refresh(entry.atom);
|
|
143
|
+
};
|
|
99
144
|
const selectQuery = (definition, args) => {
|
|
100
145
|
const { entry, atom, config } = acquireQuery(definition, args);
|
|
146
|
+
if (entry.canceled && !previousValue(entry))
|
|
147
|
+
refresh(entry);
|
|
101
148
|
if (registry.getNodes().has(atom) &&
|
|
102
149
|
entry.loadedAt !== undefined &&
|
|
103
150
|
Date.now() - entry.loadedAt >= config.staleTime) {
|
|
104
151
|
const current = registry.get(atom);
|
|
105
152
|
if (!current.waiting)
|
|
106
|
-
|
|
153
|
+
refresh(entry);
|
|
107
154
|
}
|
|
108
155
|
return atom;
|
|
109
156
|
};
|
|
157
|
+
const retain = (atom) => {
|
|
158
|
+
const entry = entriesByAtom.get(atom);
|
|
159
|
+
if (!entry)
|
|
160
|
+
return () => { };
|
|
161
|
+
entry.users++;
|
|
162
|
+
let released = false;
|
|
163
|
+
return () => {
|
|
164
|
+
if (released)
|
|
165
|
+
return;
|
|
166
|
+
released = true;
|
|
167
|
+
entry.users--;
|
|
168
|
+
if (!disposed &&
|
|
169
|
+
entry.users === 0 &&
|
|
170
|
+
entry.unused === 'cancel' &&
|
|
171
|
+
registry.getNodes().has(entry.atom) &&
|
|
172
|
+
registry.get(entry.atom).waiting)
|
|
173
|
+
registry.set(entry.atom, cancelValue);
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const abortPrefetches = (entry) => {
|
|
177
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- Cancellation can add or remove observers reentrantly.
|
|
178
|
+
for (const abort of [...entry.prefetches])
|
|
179
|
+
abort();
|
|
180
|
+
};
|
|
110
181
|
const cache = {
|
|
111
|
-
|
|
112
|
-
|
|
182
|
+
batch: Atom.batch,
|
|
183
|
+
getQueryData(definition, args) {
|
|
184
|
+
return previousValue(resources.get(queryKey(definition, args)))?.value;
|
|
185
|
+
},
|
|
186
|
+
invalidateWhere(definition, predicate) {
|
|
187
|
+
const selected = [...resources.values()].filter((entry) => entry.query === definition && predicate(entry.args));
|
|
188
|
+
Atom.batch(() => {
|
|
189
|
+
for (const entry of selected)
|
|
190
|
+
refresh(entry);
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
invalidateGroup(group) {
|
|
194
|
+
Atom.batch(() => {
|
|
195
|
+
for (const entry of resources.values())
|
|
196
|
+
if (entry.groups?.includes(group))
|
|
197
|
+
refresh(entry);
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
cancelQuery(definition, ...selected) {
|
|
201
|
+
Atom.batch(() => {
|
|
202
|
+
for (const [key, entry] of resources)
|
|
203
|
+
if (entry.query === definition &&
|
|
204
|
+
(!selected.length || key === queryKey(definition, selected[0])))
|
|
205
|
+
if (registry.getNodes().has(entry.atom) && registry.get(entry.atom).waiting) {
|
|
206
|
+
abortPrefetches(entry);
|
|
207
|
+
registry.set(entry.atom, cancelValue);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
removeQuery(definition, ...selected) {
|
|
212
|
+
Atom.batch(() => {
|
|
213
|
+
for (const [key, entry] of resources)
|
|
214
|
+
if (entry.query === definition &&
|
|
215
|
+
(!selected.length || key === queryKey(definition, selected[0])))
|
|
216
|
+
if (registry.getNodes().has(entry.atom)) {
|
|
217
|
+
abortPrefetches(entry);
|
|
218
|
+
registry.set(entry.atom, removeValue);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
prefetch(definition, args, options) {
|
|
223
|
+
return Effect.suspend(() => {
|
|
224
|
+
checkWritable();
|
|
225
|
+
const atom = selectQuery(definition, args);
|
|
226
|
+
const release = retain(atom);
|
|
227
|
+
const entry = entriesByAtom.get(atom);
|
|
228
|
+
const signal = Deferred.makeUnsafe();
|
|
229
|
+
const abort = () => {
|
|
230
|
+
Deferred.doneUnsafe(signal, Effect.interrupt);
|
|
231
|
+
};
|
|
232
|
+
entry.prefetches.add(abort);
|
|
233
|
+
return Effect.suspend(() => {
|
|
234
|
+
if (options?.refresh && registry.getNodes().has(atom) && !registry.get(atom).waiting)
|
|
235
|
+
refresh(entry);
|
|
236
|
+
return Effect.raceFirst(Deferred.await(signal), AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true }));
|
|
237
|
+
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
238
|
+
entry.prefetches.delete(abort);
|
|
239
|
+
release();
|
|
240
|
+
})));
|
|
241
|
+
});
|
|
113
242
|
},
|
|
114
243
|
setQueryData(definition, args, value) {
|
|
115
244
|
checkWritable();
|
|
@@ -140,12 +269,10 @@ function createQueryCache(runtime) {
|
|
|
140
269
|
if (selected.length) {
|
|
141
270
|
const entry = resources.get(queryKey(definition, selected[0]));
|
|
142
271
|
if (entry)
|
|
143
|
-
|
|
272
|
+
refresh(entry);
|
|
144
273
|
}
|
|
145
274
|
else {
|
|
146
|
-
|
|
147
|
-
if (entry.query === definition)
|
|
148
|
-
registry.refresh(entry.atom);
|
|
275
|
+
cache.invalidateWhere(definition, () => true);
|
|
149
276
|
}
|
|
150
277
|
},
|
|
151
278
|
onReset(listener) {
|
|
@@ -155,21 +282,43 @@ function createQueryCache(runtime) {
|
|
|
155
282
|
resetResources() {
|
|
156
283
|
Atom.batch(() => {
|
|
157
284
|
for (const entry of resources.values()) {
|
|
285
|
+
abortPrefetches(entry);
|
|
158
286
|
entry.load = () => Effect.interrupt;
|
|
159
|
-
|
|
287
|
+
refresh(entry);
|
|
160
288
|
}
|
|
161
289
|
resources.clear();
|
|
162
290
|
registry.update(generation, (value) => value + 1);
|
|
163
291
|
});
|
|
164
292
|
},
|
|
293
|
+
close() {
|
|
294
|
+
if (!closing) {
|
|
295
|
+
cache.dispose();
|
|
296
|
+
closing = settlement.wait();
|
|
297
|
+
}
|
|
298
|
+
return closing;
|
|
299
|
+
},
|
|
165
300
|
dispose() {
|
|
166
301
|
if (disposed)
|
|
167
302
|
return;
|
|
168
303
|
disposed = true;
|
|
304
|
+
for (const entry of resources.values())
|
|
305
|
+
abortPrefetches(entry);
|
|
169
306
|
registry.dispose();
|
|
170
307
|
resources.clear();
|
|
171
308
|
},
|
|
172
309
|
};
|
|
173
|
-
registerCache(cache, {
|
|
310
|
+
registerCache(cache, {
|
|
311
|
+
disposed: () => disposed,
|
|
312
|
+
refresh: (atom) => {
|
|
313
|
+
const entry = entriesByAtom.get(atom);
|
|
314
|
+
if (entry)
|
|
315
|
+
refresh(entry);
|
|
316
|
+
},
|
|
317
|
+
registry,
|
|
318
|
+
generation,
|
|
319
|
+
query: selectQuery,
|
|
320
|
+
retain,
|
|
321
|
+
revision: (definition, args) => resources.get(queryKey(definition, args))?.revision,
|
|
322
|
+
});
|
|
174
323
|
return Object.freeze(cache);
|
|
175
324
|
}
|
package/dist/component.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Transition } from './program.js';
|
|
|
4
4
|
import { type UiRuntime } from './runtime.js';
|
|
5
5
|
/** Local fields with no command lifecycle. Sends are shallow patches, never updater callbacks. */
|
|
6
6
|
export declare function localComponent<Props, State extends object>(definition: {
|
|
7
|
+
identity?: (props: Snapshot<Props>) => unknown;
|
|
7
8
|
init: (props: Snapshot<Props>) => (State | Snapshot<State>) & {
|
|
8
9
|
readonly props?: never;
|
|
9
10
|
};
|
|
@@ -17,6 +18,7 @@ export declare function localComponent<Props, State extends object>(definition:
|
|
|
17
18
|
export declare function component<Props, Model extends {
|
|
18
19
|
readonly props: Props;
|
|
19
20
|
}, Message, R = never>(definition: {
|
|
21
|
+
identity?: (props: Snapshot<Props>) => unknown;
|
|
20
22
|
init: (props: Snapshot<Props>) => Model | Snapshot<Model>;
|
|
21
23
|
receive?: (model: Snapshot<Model>, props: Snapshot<Props>) => Transition<Model, Message, R>;
|
|
22
24
|
update: (model: Snapshot<Model>, message: Message) => Transition<Model, Message, R>;
|
|
@@ -28,6 +30,7 @@ export declare function component<Props, Model extends {
|
|
|
28
30
|
})): View<Props, never>;
|
|
29
31
|
/** Mount an existing program without introducing a second state owner. */
|
|
30
32
|
export declare function programView<Props, Model, Message>(definition: {
|
|
33
|
+
identity?: (props: Snapshot<Props>) => unknown;
|
|
31
34
|
create: (props: Snapshot<Props>) => import('./program').Program<Model, Message>;
|
|
32
35
|
receive: (source: import('./program').Program<Model, Message>, props: Snapshot<Props>) => void;
|
|
33
36
|
view: View<Model, Message>;
|
package/dist/component.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { compiled, Scope } from './dom.js';
|
|
1
|
+
import { compiled, Scope, viewRegion } from './dom.js';
|
|
2
2
|
import { mapCommand, program } from './program.js';
|
|
3
3
|
import { patchModel } from './state.js';
|
|
4
|
+
import { reportSafely } from './errors.js';
|
|
4
5
|
import { defaultUiRuntime } from './runtime.js';
|
|
5
6
|
/** Local fields with no command lifecycle. Sends are shallow patches, never updater callbacks. */
|
|
6
7
|
export function localComponent(definition) {
|
|
7
8
|
return component({
|
|
9
|
+
...(definition.identity ? { identity: definition.identity } : {}),
|
|
8
10
|
init: (props) => ({ ...definition.init(props), props }),
|
|
9
11
|
update: (model, patch) => {
|
|
10
12
|
const next = patchModel(model, patch);
|
|
@@ -16,7 +18,7 @@ export function localComponent(definition) {
|
|
|
16
18
|
/** A child model receives immutable parent inputs and owns its own message/command loop. */
|
|
17
19
|
export function component(definition) {
|
|
18
20
|
const runtime = definition.runtime ?? defaultUiRuntime;
|
|
19
|
-
return compiled((scope, parent, before) => {
|
|
21
|
+
return identify(compiled((scope, parent, before) => {
|
|
20
22
|
const wrap = (next) => ({
|
|
21
23
|
model: next.model,
|
|
22
24
|
...(next.cancel ? { cancel: next.cancel } : {}),
|
|
@@ -37,22 +39,80 @@ export function component(definition) {
|
|
|
37
39
|
})
|
|
38
40
|
: definition.update(model, envelope.message)),
|
|
39
41
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
if (scope.disposed) {
|
|
43
|
+
closeProgram(scope, source);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const child = new Scope(source.model(), (message) => source.send({ type: 'Message', message }), scope.report, scope.settlement);
|
|
47
|
+
let unsubscribe = () => { };
|
|
48
|
+
scope.cleanups.push(() => closeProgram(scope, source), () => child.dispose(), () => unsubscribe());
|
|
49
|
+
const release = source.subscribe((model) => child.set(model));
|
|
50
|
+
if (scope.disposed) {
|
|
51
|
+
release();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
unsubscribe = release;
|
|
43
55
|
definition.view.build(child, parent, before);
|
|
44
56
|
scope.jobs.push(() => source.send({ type: 'Input', props: scope.value }));
|
|
45
57
|
source.send({ type: 'Input', props: scope.value });
|
|
46
|
-
});
|
|
58
|
+
}), definition.identity);
|
|
47
59
|
}
|
|
48
60
|
/** Mount an existing program without introducing a second state owner. */
|
|
49
61
|
export function programView(definition) {
|
|
50
|
-
return compiled((scope, parent, before) => {
|
|
62
|
+
return identify(compiled((scope, parent, before) => {
|
|
51
63
|
const source = definition.create(scope.value);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
if (scope.disposed) {
|
|
65
|
+
closeProgram(scope, source);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const child = new Scope(source.model(), source.send, scope.report, scope.settlement);
|
|
69
|
+
let unsubscribe = () => { };
|
|
70
|
+
scope.cleanups.push(() => closeProgram(scope, source), () => child.dispose(), () => unsubscribe());
|
|
71
|
+
const release = source.subscribe((model) => child.set(model));
|
|
72
|
+
if (scope.disposed) {
|
|
73
|
+
release();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
unsubscribe = release;
|
|
55
77
|
definition.view.build(child, parent, before);
|
|
56
78
|
scope.jobs.push(() => definition.receive(source, scope.value));
|
|
79
|
+
}), definition.identity);
|
|
80
|
+
}
|
|
81
|
+
function closeProgram(scope, source) {
|
|
82
|
+
const finish = scope.settlement.begin();
|
|
83
|
+
try {
|
|
84
|
+
if (source.close) {
|
|
85
|
+
source.close().then(finish, (error) => {
|
|
86
|
+
finish();
|
|
87
|
+
reportSafely(scope.report, error);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
source.dispose();
|
|
92
|
+
finish();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
finish();
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function identify(definition, identity) {
|
|
101
|
+
if (!identity)
|
|
102
|
+
return definition;
|
|
103
|
+
return compiled((scope, parent, before) => {
|
|
104
|
+
const render = viewRegion(scope, parent, before, { manual: true });
|
|
105
|
+
let current;
|
|
106
|
+
let active;
|
|
107
|
+
const update = () => {
|
|
108
|
+
const next = identity(scope.value);
|
|
109
|
+
if (!active || !Object.is(current, next)) {
|
|
110
|
+
current = next;
|
|
111
|
+
active = compiled(definition.build);
|
|
112
|
+
}
|
|
113
|
+
render(active);
|
|
114
|
+
};
|
|
115
|
+
scope.jobs.push(update);
|
|
116
|
+
update();
|
|
57
117
|
});
|
|
58
118
|
}
|
package/dist/dom.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type BindingSource } from './diagnostics.js';
|
|
|
4
4
|
import type { Rows } from './index.js';
|
|
5
5
|
import { jsxComponent, type JSX } from './jsx.js';
|
|
6
6
|
import type { DomMount } from './mount.js';
|
|
7
|
+
import { Settlement } from './settlement.js';
|
|
7
8
|
import type { Program, Send } from './program.js';
|
|
8
9
|
type Dependencies = () => readonly unknown[];
|
|
9
10
|
type Cleanup = () => void;
|
|
@@ -13,11 +14,12 @@ export declare class Scope<M, E> {
|
|
|
13
14
|
value: M;
|
|
14
15
|
readonly send: Send<E>;
|
|
15
16
|
readonly report: ReportError;
|
|
17
|
+
readonly settlement: Settlement;
|
|
16
18
|
readonly jobs: Cleanup[];
|
|
17
19
|
readonly cleanups: Cleanup[];
|
|
18
20
|
disposed: boolean;
|
|
19
21
|
private revision;
|
|
20
|
-
constructor(value: M, send: Send<E>, report?: ReportError);
|
|
22
|
+
constructor(value: M, send: Send<E>, report?: ReportError, settlement?: Settlement);
|
|
21
23
|
derive<A>(dependencies: Dependencies, compute: () => A, source?: BindingSource): () => A;
|
|
22
24
|
watch(dependencies: Dependencies, apply: () => void, source?: BindingSource): void;
|
|
23
25
|
set(value: M): void;
|
|
@@ -62,7 +64,10 @@ export interface PortalProps {
|
|
|
62
64
|
export declare const Portal: View<PortalProps, never>;
|
|
63
65
|
export declare function mountView<M, E>(parent: Node, definition: View<M, E>, source: Program<M, E>, options?: {
|
|
64
66
|
onError?: ReportError;
|
|
65
|
-
}): () => void
|
|
67
|
+
}): (() => void) & {
|
|
68
|
+
dispose: () => void;
|
|
69
|
+
close: () => Promise<void>;
|
|
70
|
+
};
|
|
66
71
|
export declare function element(parent: Node, before: Node | null, tag: string): Element;
|
|
67
72
|
export declare function attribute(element: Element, name: string, value: unknown): void;
|
|
68
73
|
/** Compiler output for attributes whose unchanged value needs no DOM refresh. */
|
|
@@ -80,10 +85,13 @@ export declare function each<M, E, A>(scope: Scope<M, E>, parent: Node, before:
|
|
|
80
85
|
export declare function invoke<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => readonly unknown[], build: Build<readonly unknown[], E>): void;
|
|
81
86
|
export declare function child<M, E, C, F>(scope: Scope<M, E>, parent: Node, before: Node | null, definition: View<C, F>, dependencies: Dependencies, model: () => C, send: Send<F>): void;
|
|
82
87
|
/** A late-bound compiled definition owns one replaceable DOM region. */
|
|
83
|
-
export declare function viewRegion<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null
|
|
88
|
+
export declare function viewRegion<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, options?: {
|
|
89
|
+
manual?: boolean;
|
|
90
|
+
report?: ReportError;
|
|
91
|
+
}): (next: View<M, E> | undefined) => void;
|
|
84
92
|
export declare function attach<M, E, T extends Element>(scope: Scope<M, E>, element: T, dependencies: Dependencies, read: () => DomMount<NoInfer<T>> | undefined): void;
|
|
85
93
|
export declare function portal<M, E>(scope: Scope<M, E>, build: Build<M, E>, mount?: () => Element | undefined): void;
|
|
86
94
|
/** Build static markup once per document/namespace, then clone native nodes at each use. */
|
|
87
95
|
export declare function template(build: string | ((parent: Node, before: Node | null) => void), depth?: number): (parent: Node, before: Node | null) => Node;
|
|
88
96
|
export declare function literal(parent: Node, before: Node | null, value: string): void;
|
|
89
|
-
export {};
|
|
97
|
+
export { intrinsic } from './intrinsic.js';
|