effectweb 0.2.3 → 0.3.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 +93 -1
- package/dist/AsyncContent.d.ts +2 -2
- package/dist/AsyncContent.js +6 -3
- package/dist/actions.d.ts +4 -3
- package/dist/actions.js +1 -0
- package/dist/cache-internals.d.ts +14 -0
- package/dist/cache-internals.js +11 -0
- package/dist/cache.d.ts +17 -14
- package/dist/cache.js +98 -26
- package/dist/collection.d.ts +10 -9
- package/dist/collection.js +32 -6
- package/dist/component.d.ts +8 -7
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +167 -1
- package/dist/dom-attributes.json +71 -0
- package/dist/dom.d.ts +33 -12
- package/dist/dom.js +466 -80
- package/dist/form.d.ts +61 -0
- package/dist/form.js +98 -0
- package/dist/html-attributes.d.ts +602 -0
- package/dist/html-attributes.js +1 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.js +7 -0
- package/dist/index.d.ts +12 -10
- package/dist/index.js +7 -5
- package/dist/jsx.d.ts +165 -33
- package/dist/jsx.js +2 -1
- package/dist/lazy.d.ts +23 -0
- package/dist/lazy.js +58 -0
- package/dist/load.d.ts +3 -3
- package/dist/mount.d.ts +8 -11
- package/dist/mount.js +1 -3
- package/dist/owner.d.ts +11 -9
- package/dist/owner.js +27 -36
- package/dist/pages.d.ts +23 -22
- package/dist/pages.js +15 -9
- package/dist/program.d.ts +19 -9
- package/dist/program.js +145 -49
- package/dist/query-internals.d.ts +11 -0
- package/dist/query-internals.js +10 -0
- package/dist/query.d.ts +24 -19
- package/dist/query.js +60 -7
- package/dist/resource.d.ts +4 -10
- package/dist/resource.js +14 -5
- package/dist/runtime.d.ts +1 -2
- package/dist/runtime.js +3 -3
- package/dist/session.d.ts +12 -15
- package/dist/session.js +15 -12
- package/dist/share.d.ts +5 -4
- package/dist/share.js +5 -44
- package/dist/sharing.d.ts +6 -0
- package/dist/sharing.js +93 -0
- package/dist/snapshot.d.ts +16 -5
- package/dist/snapshot.js +6 -7
- package/dist/svg-attributes.d.ts +12 -0
- package/dist/svg-attributes.js +1 -0
- package/dist/task.d.ts +9 -8
- package/dist/task.js +75 -47
- package/dist/tasks.d.ts +20 -15
- package/dist/tasks.js +39 -25
- package/dist/testing.d.ts +10 -14
- package/package.json +5 -1
package/dist/session.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Snapshot } from './snapshot.js';
|
|
2
|
+
import { type Query } from './query.js';
|
|
2
3
|
import type { DisposableOwner } from './owner.js';
|
|
3
4
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
4
5
|
import type { QueryCache } from './cache.js';
|
|
@@ -12,6 +13,14 @@ export interface SessionContext<R = never> {
|
|
|
12
13
|
readonly cache: QueryCache<R>;
|
|
13
14
|
readonly changed: () => void;
|
|
14
15
|
}
|
|
16
|
+
/** One owned selection and immutable observation of a shared query resource. */
|
|
17
|
+
export interface QueryResource<Args, A, E = never> {
|
|
18
|
+
select(this: void, args: Args | Snapshot<Args> | undefined): void;
|
|
19
|
+
read(this: void): AsyncResult.AsyncResult<Snapshot<A>, E>;
|
|
20
|
+
subscribe(this: void, listener: (result: AsyncResult.AsyncResult<Snapshot<A>, E>) => void): () => void;
|
|
21
|
+
refresh(this: void): void;
|
|
22
|
+
dispose(this: void): void;
|
|
23
|
+
}
|
|
15
24
|
/**
|
|
16
25
|
* Idempotent query reconciliation for snapshot ticks. Freshness is checked when entering a
|
|
17
26
|
* selection (including remount), not on every same-key tick; refresh revalidates unless a request is already in flight.
|
|
@@ -19,20 +28,8 @@ export interface SessionContext<R = never> {
|
|
|
19
28
|
export declare function queryResource<Args, A, E, R>(context: {
|
|
20
29
|
cache: QueryCache<R>;
|
|
21
30
|
changed?: () => void;
|
|
22
|
-
}, definition: Query<Args, A, E, NoInfer<R>>):
|
|
23
|
-
|
|
24
|
-
read: () => AsyncResult.AsyncResult<A, E>;
|
|
25
|
-
subscribe(listener: (result: AsyncResult.AsyncResult<A, E>) => void): () => void;
|
|
26
|
-
refresh: () => void;
|
|
27
|
-
dispose: () => void;
|
|
28
|
-
};
|
|
29
|
-
export declare function observeQuery<Args, A, E, R>(owner: DisposableOwner, cache: QueryCache<R>, definition: Query<Args, A, E, NoInfer<R>>, changed: (result: AsyncResult.AsyncResult<A, E>) => void): {
|
|
30
|
-
select(args: Args | undefined): void;
|
|
31
|
-
read: () => AsyncResult.AsyncResult<A, E>;
|
|
32
|
-
subscribe(listener: (result: AsyncResult.AsyncResult<A, E>) => void): () => void;
|
|
33
|
-
refresh: () => void;
|
|
34
|
-
dispose: () => void;
|
|
35
|
-
};
|
|
31
|
+
}, definition: Query<Args, A, E, NoInfer<R>>): QueryResource<Args, A, E>;
|
|
32
|
+
export declare function observeQuery<Args, A, E, R>(owner: DisposableOwner, cache: QueryCache<R>, definition: Query<Args, A, E, NoInfer<R>>, changed: (result: AsyncResult.AsyncResult<Snapshot<A>, E>) => void): QueryResource<Args, A, E>;
|
|
36
33
|
/** Application selectors run once per explicit input version, with optional prior output sharing. */
|
|
37
34
|
export declare function projectionCache(): {
|
|
38
35
|
invalidate: () => void;
|
package/dist/session.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
2
|
+
import { encodeQueryKey } from './query.js';
|
|
2
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
3
4
|
import * as Atom from 'effect/unstable/reactivity/Atom';
|
|
5
|
+
import { cacheInternals } from './cache-internals.js';
|
|
4
6
|
export function lifetime() {
|
|
5
7
|
const cleanups = [];
|
|
6
8
|
let disposed = false;
|
|
@@ -27,6 +29,7 @@ export function lifetime() {
|
|
|
27
29
|
* selection (including remount), not on every same-key tick; refresh revalidates unless a request is already in flight.
|
|
28
30
|
*/
|
|
29
31
|
export function queryResource(context, definition) {
|
|
32
|
+
const internal = cacheInternals(context.cache);
|
|
30
33
|
let key;
|
|
31
34
|
let generation = -1;
|
|
32
35
|
let atom;
|
|
@@ -35,8 +38,8 @@ export function queryResource(context, definition) {
|
|
|
35
38
|
let revision = 0;
|
|
36
39
|
const initial = AsyncResult.initial();
|
|
37
40
|
const listeners = new Set();
|
|
38
|
-
const read = () => atom && !disposed && generation ===
|
|
39
|
-
?
|
|
41
|
+
const read = () => atom && !disposed && generation === internal.registry.get(internal.generation)
|
|
42
|
+
? internal.registry.get(atom)
|
|
40
43
|
: initial;
|
|
41
44
|
let published;
|
|
42
45
|
let notifying = false;
|
|
@@ -69,7 +72,7 @@ export function queryResource(context, definition) {
|
|
|
69
72
|
published = result;
|
|
70
73
|
const selected = revision;
|
|
71
74
|
// A callback may select, reset, refresh or dispose. Finish only the current publication.
|
|
72
|
-
// oxlint-disable-next-line unicorn/no-useless-spread
|
|
75
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- Iterate a snapshot because listeners can add or remove subscriptions.
|
|
73
76
|
for (const listener of [...listeners]) {
|
|
74
77
|
if (disposed || selected !== revision || result !== read())
|
|
75
78
|
break;
|
|
@@ -88,8 +91,8 @@ export function queryResource(context, definition) {
|
|
|
88
91
|
release?.();
|
|
89
92
|
};
|
|
90
93
|
// Initialize before subscribing; construction must not invoke application callbacks.
|
|
91
|
-
|
|
92
|
-
const stopGeneration =
|
|
94
|
+
internal.registry.get(internal.generation);
|
|
95
|
+
const stopGeneration = internal.registry.subscribe(internal.generation, () => {
|
|
93
96
|
revision++;
|
|
94
97
|
disconnect();
|
|
95
98
|
atom = undefined;
|
|
@@ -101,21 +104,21 @@ export function queryResource(context, definition) {
|
|
|
101
104
|
select(args) {
|
|
102
105
|
if (disposed)
|
|
103
106
|
return;
|
|
104
|
-
const nextGeneration =
|
|
105
|
-
const nextKey = args === undefined ? undefined :
|
|
107
|
+
const nextGeneration = internal.registry.get(internal.generation);
|
|
108
|
+
const nextKey = args === undefined ? undefined : encodeQueryKey(args);
|
|
106
109
|
if (nextKey === key && nextGeneration === generation)
|
|
107
110
|
return;
|
|
108
111
|
const selected = ++revision;
|
|
109
112
|
disconnect();
|
|
110
113
|
key = nextKey;
|
|
111
114
|
generation = nextGeneration;
|
|
112
|
-
const nextAtom = args === undefined ? undefined :
|
|
115
|
+
const nextAtom = args === undefined ? undefined : internal.query(definition, args);
|
|
113
116
|
if (disposed || selected !== revision)
|
|
114
117
|
return;
|
|
115
118
|
atom = nextAtom;
|
|
116
119
|
if (nextAtom) {
|
|
117
120
|
// Subscription setup itself can execute a synchronous Effect and reenter selection.
|
|
118
|
-
const release =
|
|
121
|
+
const release = internal.registry.subscribe(nextAtom, notify);
|
|
119
122
|
if (disposed || selected !== revision) {
|
|
120
123
|
release();
|
|
121
124
|
return;
|
|
@@ -136,9 +139,9 @@ export function queryResource(context, definition) {
|
|
|
136
139
|
refresh: () => {
|
|
137
140
|
if (atom &&
|
|
138
141
|
!disposed &&
|
|
139
|
-
generation ===
|
|
140
|
-
!
|
|
141
|
-
|
|
142
|
+
generation === internal.registry.get(internal.generation) &&
|
|
143
|
+
!internal.registry.get(atom).waiting)
|
|
144
|
+
internal.registry.refresh(atom);
|
|
142
145
|
},
|
|
143
146
|
dispose: () => {
|
|
144
147
|
if (disposed)
|
package/dist/share.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Snapshot } from './snapshot.js';
|
|
2
|
+
/** Field sharers borrow both inputs; returned branches are published as readonly data. */
|
|
2
3
|
export type ShareFields<T> = {
|
|
3
|
-
readonly [K in keyof T]?: (previous: T[K]
|
|
4
|
+
readonly [K in keyof T]?: (previous: Snapshot<T[K]>, next: Snapshot<T[K]>) => T[K] | Snapshot<T[K]>;
|
|
4
5
|
};
|
|
5
|
-
/**
|
|
6
|
-
export declare function shareValue<T>(previous: T
|
|
6
|
+
/** Borrow shared immutable data. The result may retain either input and its nested branches. */
|
|
7
|
+
export declare function shareValue<T>(previous: T | Snapshot<T>, next: T | Snapshot<T>, fields?: ShareFields<T>): Snapshot<T>;
|
package/dist/share.js
CHANGED
|
@@ -1,46 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const sharedPairs = new WeakMap();
|
|
4
|
-
/** Share JSON-shaped data; Blob, typed arrays and class instances retain their own identity. */
|
|
1
|
+
import { shareData } from './sharing.js';
|
|
2
|
+
/** Borrow shared immutable data. The result may retain either input and its nested branches. */
|
|
5
3
|
export function shareValue(previous, next, fields) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return next;
|
|
10
|
-
const array = Array.isArray(next);
|
|
11
|
-
if (array !== Array.isArray(previous))
|
|
12
|
-
return next;
|
|
13
|
-
if (!array &&
|
|
14
|
-
(Object.getPrototypeOf(next) !== Object.prototype ||
|
|
15
|
-
Object.getPrototypeOf(previous) !== Object.prototype))
|
|
16
|
-
return next;
|
|
17
|
-
let pairs = sharedPairs.get(previous);
|
|
18
|
-
if (!fields && pairs?.has(next))
|
|
19
|
-
return pairs.get(next);
|
|
20
|
-
const old = previous;
|
|
21
|
-
const value = next;
|
|
22
|
-
const keys = Object.keys(value);
|
|
23
|
-
let equal = keys.length === Object.keys(old).length;
|
|
24
|
-
let unchangedNext = true;
|
|
25
|
-
const shared = array ? [] : {};
|
|
26
|
-
for (const key of keys) {
|
|
27
|
-
const custom = fields && Object.hasOwn(fields, key) ? fields[key] : undefined;
|
|
28
|
-
shared[key] =
|
|
29
|
-
custom && Object.hasOwn(old, key)
|
|
30
|
-
? custom(old[key], value[key])
|
|
31
|
-
: shareValue(old[key], value[key]);
|
|
32
|
-
if (!Object.hasOwn(old, key) || shared[key] !== old[key])
|
|
33
|
-
equal = false;
|
|
34
|
-
if (shared[key] !== value[key])
|
|
35
|
-
unchangedNext = false;
|
|
36
|
-
}
|
|
37
|
-
const result = equal ? previous : unchangedNext ? next : shared;
|
|
38
|
-
if (fields)
|
|
39
|
-
return result;
|
|
40
|
-
if (!pairs) {
|
|
41
|
-
pairs = new WeakMap();
|
|
42
|
-
sharedPairs.set(previous, pairs);
|
|
43
|
-
}
|
|
44
|
-
pairs.set(next, result);
|
|
45
|
-
return result;
|
|
4
|
+
// Snapshot changes access permissions, not representation. The private algorithm only
|
|
5
|
+
// borrows data; this boundary never exposes mutable aliases to its retained branches.
|
|
6
|
+
return shareData(previous, next, fields);
|
|
46
7
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Override sharing for selected fields, for example a collection keyed by domain identity. */
|
|
2
|
+
export type ShareFields<T> = {
|
|
3
|
+
readonly [K in keyof T]?: (previous: T[K], next: T[K]) => T[K];
|
|
4
|
+
};
|
|
5
|
+
/** Share immutable plain data; opaque objects, accessors, hidden fields and cycles retain next. */
|
|
6
|
+
export declare function shareData<T>(previous: T, next: T, fields?: ShareFields<T>): T;
|
package/dist/sharing.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Internal representation-level sharing. Public callers borrow through share.ts.
|
|
2
|
+
// Immutable snapshots can reach the renderer through several projections. Reuse an
|
|
3
|
+
// already compared object pair without retaining either snapshot after its owners release it.
|
|
4
|
+
const sharedPairs = new WeakMap();
|
|
5
|
+
const cyclicData = new WeakMap();
|
|
6
|
+
function cyclic(value, active) {
|
|
7
|
+
const cached = cyclicData.get(value);
|
|
8
|
+
if (cached !== undefined)
|
|
9
|
+
return cached;
|
|
10
|
+
const prototype = Object.getPrototypeOf(value);
|
|
11
|
+
if (!Array.isArray(value) && prototype !== Object.prototype)
|
|
12
|
+
return false;
|
|
13
|
+
if (active.has(value))
|
|
14
|
+
return true;
|
|
15
|
+
active.add(value);
|
|
16
|
+
const found = Reflect.ownKeys(value).some((key) => {
|
|
17
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
18
|
+
const child = descriptor.value;
|
|
19
|
+
return child !== null && typeof child === 'object' && cyclic(child, active);
|
|
20
|
+
});
|
|
21
|
+
active.delete(value);
|
|
22
|
+
cyclicData.set(value, found);
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
/** Share immutable plain data; opaque objects, accessors, hidden fields and cycles retain next. */
|
|
26
|
+
export function shareData(previous, next, fields) {
|
|
27
|
+
if (Object.is(previous, next))
|
|
28
|
+
return previous;
|
|
29
|
+
// Check the entire next graph, including new branches with no previous counterpart.
|
|
30
|
+
// Otherwise a partial clone could keep a back-reference to the unshared root.
|
|
31
|
+
if (next && typeof next === 'object' && cyclic(next, new Set()))
|
|
32
|
+
return next;
|
|
33
|
+
return share(previous, next, fields);
|
|
34
|
+
}
|
|
35
|
+
function share(previous, next, fields) {
|
|
36
|
+
if (Object.is(previous, next))
|
|
37
|
+
return previous;
|
|
38
|
+
if (!previous || !next || typeof previous !== 'object' || typeof next !== 'object')
|
|
39
|
+
return next;
|
|
40
|
+
const array = Array.isArray(next);
|
|
41
|
+
if (array !== Array.isArray(previous))
|
|
42
|
+
return next;
|
|
43
|
+
if (!array &&
|
|
44
|
+
(Object.getPrototypeOf(next) !== Object.prototype ||
|
|
45
|
+
Object.getPrototypeOf(previous) !== Object.prototype))
|
|
46
|
+
return next;
|
|
47
|
+
let pairs = sharedPairs.get(previous);
|
|
48
|
+
if (!fields && pairs?.has(next))
|
|
49
|
+
return pairs.get(next);
|
|
50
|
+
const old = previous;
|
|
51
|
+
const value = next;
|
|
52
|
+
const keys = Object.keys(value);
|
|
53
|
+
const oldKeys = Object.keys(old);
|
|
54
|
+
// Symbol and nonenumerable fields are outside the plain-data sharing contract.
|
|
55
|
+
// Returning next preserves them instead of accidentally claiming value equality.
|
|
56
|
+
const extra = array ? 1 : 0; // Array length is nonenumerable.
|
|
57
|
+
if (Reflect.ownKeys(next).length !== keys.length + extra ||
|
|
58
|
+
Reflect.ownKeys(previous).length !== oldKeys.length + extra ||
|
|
59
|
+
keys.some((key) => !Object.hasOwn(Object.getOwnPropertyDescriptor(next, key), 'value')) ||
|
|
60
|
+
oldKeys.some((key) => !Object.hasOwn(Object.getOwnPropertyDescriptor(previous, key), 'value')))
|
|
61
|
+
return next;
|
|
62
|
+
let equal = keys.length === oldKeys.length && (!array || value.length === old.length);
|
|
63
|
+
let unchangedNext = true;
|
|
64
|
+
const shared = array ? [] : {};
|
|
65
|
+
if (array)
|
|
66
|
+
shared.length = value.length;
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
const custom = fields && Object.hasOwn(fields, key) ? fields[key] : undefined;
|
|
69
|
+
const item = custom && Object.hasOwn(old, key)
|
|
70
|
+
? custom(old[key], value[key])
|
|
71
|
+
: share(Object.hasOwn(old, key) ? old[key] : undefined, value[key], undefined);
|
|
72
|
+
// Define an own data property, including __proto__, without invoking setters.
|
|
73
|
+
Object.defineProperty(shared, key, {
|
|
74
|
+
value: item,
|
|
75
|
+
writable: true,
|
|
76
|
+
enumerable: true,
|
|
77
|
+
configurable: true,
|
|
78
|
+
});
|
|
79
|
+
if (!Object.hasOwn(old, key) || !Object.is(item, old[key]))
|
|
80
|
+
equal = false;
|
|
81
|
+
if (!Object.is(item, value[key]))
|
|
82
|
+
unchangedNext = false;
|
|
83
|
+
}
|
|
84
|
+
const result = equal ? previous : unchangedNext ? next : shared;
|
|
85
|
+
if (fields)
|
|
86
|
+
return result;
|
|
87
|
+
if (!pairs) {
|
|
88
|
+
pairs = new WeakMap();
|
|
89
|
+
sharedPairs.set(previous, pairs);
|
|
90
|
+
}
|
|
91
|
+
pairs.set(next, result);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
1
|
+
import type { Effect } from 'effect';
|
|
2
|
+
import type * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
3
|
+
/** Mark an application service as opaque to snapshot typing; this is a type-only brand. */
|
|
4
|
+
export declare const snapshotOpaque: unique symbol;
|
|
5
|
+
export interface SnapshotOpaque {
|
|
6
|
+
readonly [snapshotOpaque]?: true;
|
|
7
|
+
}
|
|
8
|
+
/** Recursively immutable published data. Effects, functions and external resources retain their API. */
|
|
9
|
+
export type Snapshot<T> = ImmutableValue<T>;
|
|
10
|
+
type SnapshotRecord<T> = {
|
|
11
|
+
readonly [K in keyof T]: ImmutableValue<T[K]>;
|
|
12
|
+
};
|
|
13
|
+
type ImmutableValue<T> = T extends string | number | bigint | boolean | symbol | null | undefined ? T : T extends (...args: never[]) => unknown ? T : typeof snapshotOpaque extends keyof T ? symbol extends keyof T ? SnapshotRecord<T> : T : T extends AsyncResult.AsyncResult<infer A, infer E> ? AsyncResult.With<T, ImmutableValue<A>, E> : T extends Effect.Effect<unknown, unknown, unknown> | Date | RegExp | Error | PromiseLike<unknown> | Node | EventTarget | Blob | ArrayBuffer | ArrayBufferView ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<ImmutableValue<K>, ImmutableValue<V>> : T extends ReadonlySet<infer A> ? ReadonlySet<ImmutableValue<A>> : T extends object ? SnapshotRecord<T> : T;
|
|
14
|
+
/** Protect published plain data in every build. Opaque objects keep their own lifecycle. */
|
|
15
|
+
export declare function protectSnapshot<T>(value: T): T;
|
|
16
|
+
export {};
|
package/dist/snapshot.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { isAsyncResult } from 'effect/unstable/reactivity/AsyncResult';
|
|
2
|
-
export const checkSnapshotsByDefault = typeof __EFFECTWEB_DEV__ !== 'undefined' && __EFFECTWEB_DEV__;
|
|
3
2
|
const checked = new WeakSet();
|
|
4
|
-
/**
|
|
5
|
-
export function protectSnapshot(value
|
|
6
|
-
if (!
|
|
3
|
+
/** Protect published plain data in every build. Opaque objects keep their own lifecycle. */
|
|
4
|
+
export function protectSnapshot(value) {
|
|
5
|
+
if (!value || typeof value !== 'object' || checked.has(value))
|
|
7
6
|
return value;
|
|
8
7
|
// AsyncResult is an immutable Effect wrapper, but its successful UI data is plain data.
|
|
9
8
|
if (isAsyncResult(value)) {
|
|
10
9
|
checked.add(value);
|
|
11
10
|
if (value._tag === 'Success')
|
|
12
|
-
protectSnapshot(value.value
|
|
11
|
+
protectSnapshot(value.value);
|
|
13
12
|
else if (value._tag === 'Failure' && value.previousSuccess._tag === 'Some')
|
|
14
|
-
protectSnapshot(value.previousSuccess.value
|
|
13
|
+
protectSnapshot(value.previousSuccess.value);
|
|
15
14
|
return value;
|
|
16
15
|
}
|
|
17
16
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -24,7 +23,7 @@ export function protectSnapshot(value, enabled) {
|
|
|
24
23
|
const descriptor = Reflect.get(descriptors, key);
|
|
25
24
|
// Never execute getters or traverse closures, class instances, Effects or DOM objects.
|
|
26
25
|
if ('value' in descriptor)
|
|
27
|
-
protectSnapshot(descriptor.value
|
|
26
|
+
protectSnapshot(descriptor.value);
|
|
28
27
|
}
|
|
29
28
|
Object.freeze(value);
|
|
30
29
|
return value;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** SVG content and presentation attributes, including Filter Effects and SMIL.
|
|
2
|
+
* https://svgwg.org/svg2-draft/attindex.html
|
|
3
|
+
* Browser spellings are preserved: animated IDL objects are never assigned as attributes.
|
|
4
|
+
*/
|
|
5
|
+
export type SvgAttributes = Partial<Record<'accumulate' | 'additive' | 'alignment-baseline' | 'amplitude' | 'attributeName' | 'attributeType' | 'azimuth' | 'baseFrequency' | 'baseline-shift' | 'begin' | 'bias' | 'by' | 'calcMode' | 'clip' | 'clip-path' | 'clip-rule' | 'clipPathUnits' | 'color' | 'color-interpolation' | 'color-interpolation-filters' | 'crossorigin' | 'cursor' | 'cx' | 'cy' | 'd' | 'diffuseConstant' | 'direction' | 'display' | 'divisor' | 'dominant-baseline' | 'dur' | 'dx' | 'dy' | 'edgeMode' | 'elevation' | 'end' | 'exponent' | 'fill' | 'fill-opacity' | 'fill-rule' | 'filter' | 'filterUnits' | 'flood-color' | 'flood-opacity' | 'font-family' | 'font-size' | 'font-size-adjust' | 'font-stretch' | 'font-style' | 'font-variant' | 'font-weight' | 'fr' | 'from' | 'fx' | 'fy' | 'glyph-orientation-horizontal' | 'glyph-orientation-vertical' | 'gradientTransform' | 'gradientUnits' | 'height' | 'href' | 'hreflang' | 'image-rendering' | 'in' | 'in2' | 'intercept' | 'k1' | 'k2' | 'k3' | 'k4' | 'kernelMatrix' | 'kernelUnitLength' | 'keyPoints' | 'keySplines' | 'keyTimes' | 'lang' | 'lengthAdjust' | 'letter-spacing' | 'lighting-color' | 'limitingConeAngle' | 'marker-end' | 'marker-mid' | 'marker-start' | 'markerHeight' | 'markerUnits' | 'markerWidth' | 'mask' | 'mask-type' | 'maskContentUnits' | 'maskUnits' | 'max' | 'media' | 'method' | 'min' | 'mode' | 'numOctaves' | 'offset' | 'opacity' | 'operator' | 'order' | 'orient' | 'origin' | 'overflow' | 'paint-order' | 'path' | 'pathLength' | 'patternContentUnits' | 'patternTransform' | 'patternUnits' | 'ping' | 'pointer-events' | 'points' | 'pointsAtX' | 'pointsAtY' | 'pointsAtZ' | 'preserveAspectRatio' | 'primitiveUnits' | 'r' | 'radius' | 'refX' | 'refY' | 'repeatCount' | 'repeatDur' | 'requiredExtensions' | 'restart' | 'result' | 'rotate' | 'rx' | 'ry' | 'scale' | 'seed' | 'shape-rendering' | 'side' | 'slope' | 'spacing' | 'specularConstant' | 'specularExponent' | 'spreadMethod' | 'startOffset' | 'stdDeviation' | 'stitchTiles' | 'stop-color' | 'stop-opacity' | 'stroke' | 'stroke-dasharray' | 'stroke-dashoffset' | 'stroke-linecap' | 'stroke-linejoin' | 'stroke-miterlimit' | 'stroke-opacity' | 'stroke-width' | 'surfaceScale' | 'systemLanguage' | 'tableValues' | 'target' | 'targetX' | 'targetY' | 'text-anchor' | 'text-decoration' | 'text-rendering' | 'textLength' | 'to' | 'transform' | 'transform-origin' | 'type' | 'unicode-bidi' | 'values' | 'vector-effect' | 'viewBox' | 'visibility' | 'width' | 'word-spacing' | 'writing-mode' | 'x' | 'x1' | 'x2' | 'xChannelSelector' | 'xmlns' | 'y' | 'y1' | 'y2' | 'yChannelSelector' | 'z', string | number | undefined>> & {
|
|
6
|
+
/** SVG uses an enumerated string here, not an HTML presence attribute. */
|
|
7
|
+
preserveAlpha?: 'true' | 'false' | undefined;
|
|
8
|
+
};
|
|
9
|
+
/** Presentation attributes shared by SVG tags whose names also exist in HTML.
|
|
10
|
+
* https://svgwg.org/svg2-draft/styling.html#PresentationAttributes
|
|
11
|
+
*/
|
|
12
|
+
export type SvgPresentationAttributes = Pick<SvgAttributes, 'alignment-baseline' | 'baseline-shift' | 'clip' | 'clip-path' | 'clip-rule' | 'color' | 'color-interpolation' | 'color-interpolation-filters' | 'cursor' | 'direction' | 'display' | 'dominant-baseline' | 'fill' | 'fill-opacity' | 'fill-rule' | 'filter' | 'flood-color' | 'flood-opacity' | 'font-family' | 'font-size' | 'font-size-adjust' | 'font-stretch' | 'font-style' | 'font-variant' | 'font-weight' | 'glyph-orientation-horizontal' | 'glyph-orientation-vertical' | 'image-rendering' | 'letter-spacing' | 'lighting-color' | 'marker-end' | 'marker-mid' | 'marker-start' | 'mask' | 'mask-type' | 'opacity' | 'overflow' | 'paint-order' | 'pointer-events' | 'shape-rendering' | 'stop-color' | 'stop-opacity' | 'stroke' | 'stroke-dasharray' | 'stroke-dashoffset' | 'stroke-linecap' | 'stroke-linejoin' | 'stroke-miterlimit' | 'stroke-opacity' | 'stroke-width' | 'text-anchor' | 'text-decoration' | 'text-rendering' | 'transform' | 'transform-origin' | 'unicode-bidi' | 'vector-effect' | 'visibility' | 'word-spacing' | 'writing-mode'>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/task.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { Snapshot } from './snapshot.js';
|
|
1
2
|
import { Effect } from 'effect';
|
|
2
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
3
4
|
import type { View } from './dom.js';
|
|
4
|
-
import { type Send } from './program.js';
|
|
5
|
+
import { type Send, type TaskPolicy } from './program.js';
|
|
5
6
|
import { type UiRuntime } from './runtime.js';
|
|
6
7
|
export type TaskModel<Props, State, A, E = unknown> = State & {
|
|
7
8
|
readonly props: Props;
|
|
@@ -12,7 +13,7 @@ export type TaskMessage<State, Input> = {
|
|
|
12
13
|
input: Input;
|
|
13
14
|
} | {
|
|
14
15
|
type: 'Fields';
|
|
15
|
-
fields: Partial<State> & {
|
|
16
|
+
fields: (Partial<State> | Partial<Snapshot<State>>) & {
|
|
16
17
|
props?: never;
|
|
17
18
|
task?: never;
|
|
18
19
|
};
|
|
@@ -25,16 +26,16 @@ export type TaskMessage<State, Input> = {
|
|
|
25
26
|
* defineTasks(...).tasks(...).view(view(...)) to establish inference before JSX.
|
|
26
27
|
*/
|
|
27
28
|
export declare function taskComponent<Props, State extends object, Input, A, E = unknown, R = never>(definition: {
|
|
28
|
-
init: (props: Props) => State & {
|
|
29
|
+
init: (props: Snapshot<Props>) => (State | Snapshot<State>) & {
|
|
29
30
|
props?: never;
|
|
30
31
|
task?: never;
|
|
31
32
|
};
|
|
32
|
-
identity?: (props: Props) => unknown;
|
|
33
|
+
identity?: (props: Snapshot<Props>) => unknown;
|
|
33
34
|
task: {
|
|
34
|
-
policy:
|
|
35
|
-
run: (model: State & {
|
|
35
|
+
policy: Exclude<TaskPolicy, 'parallel'>;
|
|
36
|
+
run: (model: Snapshot<State & {
|
|
36
37
|
readonly props: Props;
|
|
37
|
-
}
|
|
38
|
+
}>, input: Input) => Effect.Effect<A, E, R>;
|
|
38
39
|
};
|
|
39
40
|
view: View<TaskModel<Props, State, A, E>, TaskMessage<State, Input>>;
|
|
40
41
|
} & ([R] extends [never] ? {
|
|
@@ -45,7 +46,7 @@ export declare function taskComponent<Props, State extends object, Input, A, E =
|
|
|
45
46
|
/** Stable methods when bound once in a compiled view's const declaration. */
|
|
46
47
|
export declare function taskControls<State, Input>(send: Send<TaskMessage<State, Input>>): {
|
|
47
48
|
run: (input: Input) => void;
|
|
48
|
-
patch: (fields: Partial<State> & {
|
|
49
|
+
patch: (fields: (Partial<State> | Partial<Snapshot<State>>) & {
|
|
49
50
|
props?: never;
|
|
50
51
|
task?: never;
|
|
51
52
|
}) => void;
|
package/dist/task.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { commandSlot } from './program.js';
|
|
1
2
|
import { Cause, Effect, Option } from 'effect';
|
|
2
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
3
|
-
import {
|
|
4
|
-
import { effectCommand } from './program.js';
|
|
4
|
+
import { programView } from './component.js';
|
|
5
|
+
import { effectCommand, } from './program.js';
|
|
5
6
|
import { patchModel } from './state.js';
|
|
6
7
|
import { defaultUiRuntime } from './runtime.js';
|
|
8
|
+
const commandTask = commandSlot('task');
|
|
7
9
|
/**
|
|
8
10
|
* One owned async action and local editable fields; policy is part of its public behavior.
|
|
9
11
|
* Compatibility factory for existing single-task components. New components can use
|
|
@@ -11,52 +13,78 @@ import { defaultUiRuntime } from './runtime.js';
|
|
|
11
13
|
*/
|
|
12
14
|
export function taskComponent(definition) {
|
|
13
15
|
const runtime = definition.runtime ?? defaultUiRuntime;
|
|
14
|
-
const init = (props) => ({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
16
|
+
const init = (props) => ({ ...definition.init(props), props, task: AsyncResult.initial() });
|
|
17
|
+
const owners = new WeakMap();
|
|
18
|
+
return programView({
|
|
19
|
+
create(props) {
|
|
20
|
+
const source = runtime.program({
|
|
21
|
+
initial: init(props),
|
|
22
|
+
update: (snapshot, message) => {
|
|
23
|
+
// The public task receives the immutable snapshot; reconstruction stays internal.
|
|
24
|
+
const model = snapshot;
|
|
25
|
+
switch (message.type) {
|
|
26
|
+
case 'Input':
|
|
27
|
+
return definition.identity &&
|
|
28
|
+
!Object.is(definition.identity(model.props), definition.identity(message.props))
|
|
29
|
+
? { model: init(message.props), cancel: [commandTask] }
|
|
30
|
+
: {
|
|
31
|
+
model: Object.is(model.props, message.props)
|
|
32
|
+
? model
|
|
33
|
+
: { ...model, props: message.props },
|
|
34
|
+
};
|
|
35
|
+
case 'Fields': {
|
|
36
|
+
const next = patchModel(model, message.fields);
|
|
37
|
+
return {
|
|
38
|
+
model: next === model ? model : { ...next, props: model.props, task: model.task },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
case 'Run':
|
|
42
|
+
if (definition.task.policy === 'drop' && model.task.waiting)
|
|
43
|
+
return { model };
|
|
44
|
+
return {
|
|
45
|
+
model: { ...model, task: AsyncResult.waiting(model.task) },
|
|
46
|
+
commands: [
|
|
47
|
+
{
|
|
48
|
+
...effectCommand(commandTask, () => definition.task.run(snapshot, message.input), {
|
|
49
|
+
policy: definition.task.policy,
|
|
50
|
+
onSuccess: (value) => ({ type: 'Succeeded', value }),
|
|
51
|
+
onFailure: (cause) => ({ type: 'Failed', cause }),
|
|
52
|
+
}),
|
|
53
|
+
policy: definition.task.policy,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
case 'Cancel':
|
|
58
|
+
return {
|
|
59
|
+
model: { ...model, task: AsyncResult.initial() },
|
|
60
|
+
cancel: [commandTask],
|
|
61
|
+
};
|
|
62
|
+
case 'Succeeded':
|
|
63
|
+
return {
|
|
64
|
+
model: {
|
|
65
|
+
...model,
|
|
66
|
+
task: AsyncResult.success(message.value, {
|
|
67
|
+
waiting: source.activeSlots().includes(commandTask),
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
case 'Failed':
|
|
72
|
+
return {
|
|
73
|
+
model: {
|
|
74
|
+
...model,
|
|
75
|
+
task: AsyncResult.failureWithPrevious(message.cause, {
|
|
76
|
+
previous: Option.some(model.task),
|
|
77
|
+
waiting: source.activeSlots().includes(commandTask),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
owners.set(source, source);
|
|
85
|
+
return source;
|
|
59
86
|
},
|
|
87
|
+
receive: (source, props) => owners.get(source).send({ type: 'Input', props }),
|
|
60
88
|
view: definition.view,
|
|
61
89
|
});
|
|
62
90
|
}
|