effectweb 0.2.2 → 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/LICENSE +1 -1
- package/README.md +94 -2
- package/dist/AsyncContent.d.ts +2 -2
- package/dist/AsyncContent.js +36 -24
- package/dist/actions.d.ts +4 -4
- package/dist/actions.js +1 -1
- package/dist/cache-internals.d.ts +14 -0
- package/dist/cache-internals.js +11 -0
- package/dist/cache.d.ts +17 -15
- package/dist/cache.js +98 -27
- package/dist/collection.d.ts +10 -10
- 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 +36 -13
- package/dist/dom.js +547 -84
- 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 -4
- 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 -38
- package/dist/pages.d.ts +23 -23
- package/dist/pages.js +15 -10
- package/dist/program.d.ts +19 -10
- package/dist/program.js +172 -66
- package/dist/query-internals.d.ts +11 -0
- package/dist/query-internals.js +10 -0
- package/dist/query.d.ts +24 -20
- 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 -18
- package/dist/session.js +15 -15
- 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/state.d.ts +0 -1
- package/dist/state.js +0 -1
- 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 -15
- package/package.json +5 -1
package/dist/resource.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
4
|
import { loadEffect } from './load.js';
|
|
4
5
|
import { component } from './component.js';
|
|
5
6
|
import { defaultUiRuntime } from './runtime.js';
|
|
6
7
|
import { effectCommand } from './program.js';
|
|
8
|
+
const commandLoad = commandSlot('load');
|
|
7
9
|
export const available = (result) => Option.getOrUndefined(AsyncResult.value(result));
|
|
8
10
|
export const resourceError = (result) => {
|
|
9
11
|
if (!AsyncResult.isFailure(result))
|
|
@@ -21,7 +23,7 @@ export function resourceComponent(definition) {
|
|
|
21
23
|
if (!selected)
|
|
22
24
|
return {
|
|
23
25
|
model: { ...model, key: undefined, result: AsyncResult.initial() },
|
|
24
|
-
cancel: [
|
|
26
|
+
cancel: [commandLoad],
|
|
25
27
|
};
|
|
26
28
|
return {
|
|
27
29
|
model: {
|
|
@@ -30,9 +32,10 @@ export function resourceComponent(definition) {
|
|
|
30
32
|
result: AsyncResult.waiting(selected.key === model.key ? model.result : AsyncResult.initial()),
|
|
31
33
|
},
|
|
32
34
|
commands: [
|
|
33
|
-
effectCommand(
|
|
35
|
+
effectCommand(commandLoad, () => runtime.provide(selected.delay
|
|
34
36
|
? Effect.sleep(selected.delay).pipe(Effect.andThen(loadEffect(selected.load)))
|
|
35
37
|
: loadEffect(selected.load)), {
|
|
38
|
+
policy: 'replace',
|
|
36
39
|
onSuccess: (value) => ({ type: 'Loaded', value }),
|
|
37
40
|
onFailure: (cause) => ({ type: 'Failed', cause }),
|
|
38
41
|
}),
|
|
@@ -40,9 +43,15 @@ export function resourceComponent(definition) {
|
|
|
40
43
|
};
|
|
41
44
|
};
|
|
42
45
|
return component({
|
|
43
|
-
init: (props) => ({
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
init: (props) => ({
|
|
47
|
+
props: props,
|
|
48
|
+
key: undefined,
|
|
49
|
+
result: AsyncResult.initial(),
|
|
50
|
+
}),
|
|
51
|
+
receive: (model, props) => request({ ...model, props: props }),
|
|
52
|
+
update: (snapshot, message) => {
|
|
53
|
+
// Internal commands preserve domain types; the public reducer boundary is immutable.
|
|
54
|
+
const model = snapshot;
|
|
46
55
|
switch (message.type) {
|
|
47
56
|
case 'Retry':
|
|
48
57
|
return request(model, true);
|
package/dist/runtime.d.ts
CHANGED
|
@@ -7,10 +7,9 @@ export interface UiRuntime<R> {
|
|
|
7
7
|
readonly provide: <A, E>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E>;
|
|
8
8
|
readonly command: <M>(command: Command<M, R>) => Command<M>;
|
|
9
9
|
readonly program: <M, Msg>(options: {
|
|
10
|
-
initial: M
|
|
10
|
+
initial: M | Snapshot<M>;
|
|
11
11
|
update: (model: Snapshot<M>, message: Msg) => Transition<M, Msg, R>;
|
|
12
12
|
name?: string;
|
|
13
|
-
checkSnapshots?: boolean;
|
|
14
13
|
onDefect?: (cause: unknown) => void;
|
|
15
14
|
}) => RunningProgram<M, Msg>;
|
|
16
15
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -3,10 +3,10 @@ import { program } from './program.js';
|
|
|
3
3
|
export function uiRuntime(context) {
|
|
4
4
|
const provide = (effect) => Effect.provideContext(effect, context);
|
|
5
5
|
const command = (value) => value.stream
|
|
6
|
-
? {
|
|
6
|
+
? { ...value, stream: value.stream.pipe(Stream.provideContext(context)) }
|
|
7
7
|
: value.action
|
|
8
|
-
? {
|
|
9
|
-
: {
|
|
8
|
+
? { ...value, action: provide(value.action) }
|
|
9
|
+
: { ...value, effect: provide(value.effect) };
|
|
10
10
|
return {
|
|
11
11
|
context,
|
|
12
12
|
provide,
|
package/dist/session.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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';
|
|
5
|
-
/** Own subscriptions, DOM adapters and other cleanup for one lifetime. */
|
|
6
6
|
export declare function lifetime(): {
|
|
7
7
|
add(cleanup: () => void): void;
|
|
8
8
|
dispose(): void;
|
|
@@ -13,6 +13,14 @@ export interface SessionContext<R = never> {
|
|
|
13
13
|
readonly cache: QueryCache<R>;
|
|
14
14
|
readonly changed: () => void;
|
|
15
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
|
+
}
|
|
16
24
|
/**
|
|
17
25
|
* Idempotent query reconciliation for snapshot ticks. Freshness is checked when entering a
|
|
18
26
|
* selection (including remount), not on every same-key tick; refresh revalidates unless a request is already in flight.
|
|
@@ -20,21 +28,8 @@ export interface SessionContext<R = never> {
|
|
|
20
28
|
export declare function queryResource<Args, A, E, R>(context: {
|
|
21
29
|
cache: QueryCache<R>;
|
|
22
30
|
changed?: () => void;
|
|
23
|
-
}, definition: Query<Args, A, E, NoInfer<R>>):
|
|
24
|
-
|
|
25
|
-
read: () => AsyncResult.AsyncResult<A, E>;
|
|
26
|
-
subscribe(listener: (result: AsyncResult.AsyncResult<A, E>) => void): () => void;
|
|
27
|
-
refresh: () => void;
|
|
28
|
-
dispose: () => void;
|
|
29
|
-
};
|
|
30
|
-
/** Publish Effect results directly; the owner releases the subscription on disposal. */
|
|
31
|
-
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): {
|
|
32
|
-
select(args: Args | undefined): void;
|
|
33
|
-
read: () => AsyncResult.AsyncResult<A, E>;
|
|
34
|
-
subscribe(listener: (result: AsyncResult.AsyncResult<A, E>) => void): () => void;
|
|
35
|
-
refresh: () => void;
|
|
36
|
-
dispose: () => void;
|
|
37
|
-
};
|
|
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>;
|
|
38
33
|
/** Application selectors run once per explicit input version, with optional prior output sharing. */
|
|
39
34
|
export declare function projectionCache(): {
|
|
40
35
|
invalidate: () => void;
|
|
@@ -48,7 +43,6 @@ interface OwnedSession {
|
|
|
48
43
|
refresh?(): void;
|
|
49
44
|
subscribe?(changed: () => void): () => void;
|
|
50
45
|
}
|
|
51
|
-
/** One declaration owns subscriptions, refresh order and disposal for a group of sessions. */
|
|
52
46
|
export declare function sessionGroup(sessions: readonly OwnedSession[], changed: () => void): {
|
|
53
47
|
refresh: () => void;
|
|
54
48
|
dispose: () => void;
|
package/dist/session.js
CHANGED
|
@@ -1,7 +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';
|
|
4
|
-
|
|
5
|
+
import { cacheInternals } from './cache-internals.js';
|
|
5
6
|
export function lifetime() {
|
|
6
7
|
const cleanups = [];
|
|
7
8
|
let disposed = false;
|
|
@@ -28,6 +29,7 @@ export function lifetime() {
|
|
|
28
29
|
* selection (including remount), not on every same-key tick; refresh revalidates unless a request is already in flight.
|
|
29
30
|
*/
|
|
30
31
|
export function queryResource(context, definition) {
|
|
32
|
+
const internal = cacheInternals(context.cache);
|
|
31
33
|
let key;
|
|
32
34
|
let generation = -1;
|
|
33
35
|
let atom;
|
|
@@ -36,8 +38,8 @@ export function queryResource(context, definition) {
|
|
|
36
38
|
let revision = 0;
|
|
37
39
|
const initial = AsyncResult.initial();
|
|
38
40
|
const listeners = new Set();
|
|
39
|
-
const read = () => atom && !disposed && generation ===
|
|
40
|
-
?
|
|
41
|
+
const read = () => atom && !disposed && generation === internal.registry.get(internal.generation)
|
|
42
|
+
? internal.registry.get(atom)
|
|
41
43
|
: initial;
|
|
42
44
|
let published;
|
|
43
45
|
let notifying = false;
|
|
@@ -70,7 +72,7 @@ export function queryResource(context, definition) {
|
|
|
70
72
|
published = result;
|
|
71
73
|
const selected = revision;
|
|
72
74
|
// A callback may select, reset, refresh or dispose. Finish only the current publication.
|
|
73
|
-
// 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.
|
|
74
76
|
for (const listener of [...listeners]) {
|
|
75
77
|
if (disposed || selected !== revision || result !== read())
|
|
76
78
|
break;
|
|
@@ -89,8 +91,8 @@ export function queryResource(context, definition) {
|
|
|
89
91
|
release?.();
|
|
90
92
|
};
|
|
91
93
|
// Initialize before subscribing; construction must not invoke application callbacks.
|
|
92
|
-
|
|
93
|
-
const stopGeneration =
|
|
94
|
+
internal.registry.get(internal.generation);
|
|
95
|
+
const stopGeneration = internal.registry.subscribe(internal.generation, () => {
|
|
94
96
|
revision++;
|
|
95
97
|
disconnect();
|
|
96
98
|
atom = undefined;
|
|
@@ -102,21 +104,21 @@ export function queryResource(context, definition) {
|
|
|
102
104
|
select(args) {
|
|
103
105
|
if (disposed)
|
|
104
106
|
return;
|
|
105
|
-
const nextGeneration =
|
|
106
|
-
const nextKey = args === undefined ? undefined :
|
|
107
|
+
const nextGeneration = internal.registry.get(internal.generation);
|
|
108
|
+
const nextKey = args === undefined ? undefined : encodeQueryKey(args);
|
|
107
109
|
if (nextKey === key && nextGeneration === generation)
|
|
108
110
|
return;
|
|
109
111
|
const selected = ++revision;
|
|
110
112
|
disconnect();
|
|
111
113
|
key = nextKey;
|
|
112
114
|
generation = nextGeneration;
|
|
113
|
-
const nextAtom = args === undefined ? undefined :
|
|
115
|
+
const nextAtom = args === undefined ? undefined : internal.query(definition, args);
|
|
114
116
|
if (disposed || selected !== revision)
|
|
115
117
|
return;
|
|
116
118
|
atom = nextAtom;
|
|
117
119
|
if (nextAtom) {
|
|
118
120
|
// Subscription setup itself can execute a synchronous Effect and reenter selection.
|
|
119
|
-
const release =
|
|
121
|
+
const release = internal.registry.subscribe(nextAtom, notify);
|
|
120
122
|
if (disposed || selected !== revision) {
|
|
121
123
|
release();
|
|
122
124
|
return;
|
|
@@ -137,9 +139,9 @@ export function queryResource(context, definition) {
|
|
|
137
139
|
refresh: () => {
|
|
138
140
|
if (atom &&
|
|
139
141
|
!disposed &&
|
|
140
|
-
generation ===
|
|
141
|
-
!
|
|
142
|
-
|
|
142
|
+
generation === internal.registry.get(internal.generation) &&
|
|
143
|
+
!internal.registry.get(atom).waiting)
|
|
144
|
+
internal.registry.refresh(atom);
|
|
143
145
|
},
|
|
144
146
|
dispose: () => {
|
|
145
147
|
if (disposed)
|
|
@@ -154,7 +156,6 @@ export function queryResource(context, definition) {
|
|
|
154
156
|
},
|
|
155
157
|
};
|
|
156
158
|
}
|
|
157
|
-
/** Publish Effect results directly; the owner releases the subscription on disposal. */
|
|
158
159
|
export function observeQuery(owner, cache, definition, changed) {
|
|
159
160
|
const resource = queryResource({ cache }, definition);
|
|
160
161
|
resource.subscribe(changed);
|
|
@@ -180,7 +181,6 @@ export function projectionCache() {
|
|
|
180
181
|
select,
|
|
181
182
|
};
|
|
182
183
|
}
|
|
183
|
-
/** One declaration owns subscriptions, refresh order and disposal for a group of sessions. */
|
|
184
184
|
export function sessionGroup(sessions, changed) {
|
|
185
185
|
const scope = lifetime();
|
|
186
186
|
for (const session of sessions) {
|
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;
|
package/dist/state.d.ts
CHANGED
package/dist/state.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Shallow immutable update; unchanged fields preserve identity and skip publication. */
|
|
2
1
|
export function patchModel(model, patch) {
|
|
3
2
|
return Reflect.ownKeys(patch).some((key) => Object.prototype.propertyIsEnumerable.call(patch, key) &&
|
|
4
3
|
(!Object.hasOwn(model, key) || !Object.is(model[key], patch[key])))
|
|
@@ -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;
|