effectweb 0.2.3 → 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/README.md +93 -1
- package/dist/AsyncContent.d.ts +2 -2
- package/dist/AsyncContent.js +7 -4
- package/dist/actions.d.ts +4 -3
- package/dist/actions.js +1 -0
- package/dist/boundary.d.ts +11 -0
- package/dist/boundary.js +66 -0
- package/dist/cache-internals.d.ts +18 -0
- package/dist/cache-internals.js +11 -0
- package/dist/cache.d.ts +35 -16
- package/dist/cache.js +258 -37
- package/dist/collection.d.ts +10 -9
- package/dist/collection.js +32 -6
- package/dist/component.d.ts +11 -7
- package/dist/component.js +70 -10
- 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 +44 -15
- package/dist/dom.js +493 -89
- package/dist/effectEvent.d.ts +2 -1
- package/dist/effectEvent.js +4 -1
- 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 +16 -11
- package/dist/index.js +11 -6
- 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/jsx.d.ts +165 -33
- package/dist/jsx.js +2 -1
- package/dist/keyed-tasks.d.ts +29 -0
- package/dist/keyed-tasks.js +97 -0
- package/dist/lazy.d.ts +23 -0
- package/dist/lazy.js +66 -0
- package/dist/load.d.ts +3 -3
- package/dist/mount.d.ts +11 -12
- package/dist/mount.js +30 -14
- package/dist/owner.d.ts +14 -10
- package/dist/owner.js +66 -39
- package/dist/pages.d.ts +28 -26
- package/dist/pages.js +19 -11
- package/dist/program.d.ts +29 -9
- package/dist/program.js +204 -61
- package/dist/query-internals.d.ts +13 -0
- package/dist/query-internals.js +10 -0
- package/dist/query.d.ts +30 -19
- package/dist/query.js +64 -5
- 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 +25 -13
- package/dist/settlement.d.ts +7 -0
- package/dist/settlement.js +27 -0
- package/dist/share.d.ts +5 -4
- package/dist/share.js +5 -44
- package/dist/sharing.d.ts +6 -0
- package/dist/sharing.js +96 -0
- package/dist/slotIdentity.d.ts +1 -0
- package/dist/slotIdentity.js +2 -0
- package/dist/snapshot.d.ts +16 -5
- package/dist/snapshot.js +34 -27
- 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 +40 -25
- package/dist/testing.d.ts +10 -14
- package/package.json +13 -1
package/dist/resource.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Snapshot } from './snapshot.js';
|
|
2
2
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
3
3
|
import type { UiLoad } from './load.js';
|
|
4
4
|
import { type UiRuntime } from './runtime.js';
|
|
@@ -8,25 +8,19 @@ export interface ResourceModel<Props, A, E = unknown> {
|
|
|
8
8
|
readonly key: string | undefined;
|
|
9
9
|
readonly result: AsyncResult.AsyncResult<A, E>;
|
|
10
10
|
}
|
|
11
|
-
export type ResourceMessage
|
|
11
|
+
export type ResourceMessage = {
|
|
12
12
|
type: 'Retry';
|
|
13
|
-
} | {
|
|
14
|
-
type: 'Loaded';
|
|
15
|
-
value: A;
|
|
16
|
-
} | {
|
|
17
|
-
type: 'Failed';
|
|
18
|
-
cause: Cause.Cause<E>;
|
|
19
13
|
};
|
|
20
14
|
export declare const available: <A>(result: AsyncResult.AsyncResult<A, unknown>) => A | undefined;
|
|
21
15
|
export declare const resourceError: (result: AsyncResult.AsyncResult<unknown, unknown>) => string;
|
|
22
16
|
/** A replaceable request owns one result. Key changes clear it; refreshes preserve the last success. */
|
|
23
17
|
export declare function resourceComponent<Props, A, E = unknown, R = never>(definition: {
|
|
24
|
-
request: (props: Props) => {
|
|
18
|
+
request: (props: Snapshot<Props>) => {
|
|
25
19
|
key: string;
|
|
26
20
|
load: () => UiLoad<A, E, R>;
|
|
27
21
|
delay?: number;
|
|
28
22
|
} | undefined;
|
|
29
|
-
view: View<ResourceModel<Props, A, E>, ResourceMessage
|
|
23
|
+
view: View<ResourceModel<Props, A, E>, ResourceMessage>;
|
|
30
24
|
} & ([R] extends [never] ? {
|
|
31
25
|
runtime?: UiRuntime<R>;
|
|
32
26
|
} : {
|
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,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,11 @@ 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 &&
|
|
39
|
-
|
|
41
|
+
const read = () => atom &&
|
|
42
|
+
!disposed &&
|
|
43
|
+
!internal.disposed() &&
|
|
44
|
+
generation === internal.registry.get(internal.generation)
|
|
45
|
+
? internal.registry.get(atom)
|
|
40
46
|
: initial;
|
|
41
47
|
let published;
|
|
42
48
|
let notifying = false;
|
|
@@ -69,7 +75,7 @@ export function queryResource(context, definition) {
|
|
|
69
75
|
published = result;
|
|
70
76
|
const selected = revision;
|
|
71
77
|
// A callback may select, reset, refresh or dispose. Finish only the current publication.
|
|
72
|
-
// oxlint-disable-next-line unicorn/no-useless-spread
|
|
78
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- Iterate a snapshot because listeners can add or remove subscriptions.
|
|
73
79
|
for (const listener of [...listeners]) {
|
|
74
80
|
if (disposed || selected !== revision || result !== read())
|
|
75
81
|
break;
|
|
@@ -88,8 +94,8 @@ export function queryResource(context, definition) {
|
|
|
88
94
|
release?.();
|
|
89
95
|
};
|
|
90
96
|
// Initialize before subscribing; construction must not invoke application callbacks.
|
|
91
|
-
|
|
92
|
-
const stopGeneration =
|
|
97
|
+
internal.registry.get(internal.generation);
|
|
98
|
+
const stopGeneration = internal.registry.subscribe(internal.generation, () => {
|
|
93
99
|
revision++;
|
|
94
100
|
disconnect();
|
|
95
101
|
atom = undefined;
|
|
@@ -99,23 +105,28 @@ export function queryResource(context, definition) {
|
|
|
99
105
|
});
|
|
100
106
|
return {
|
|
101
107
|
select(args) {
|
|
102
|
-
if (disposed)
|
|
108
|
+
if (disposed || internal.disposed())
|
|
103
109
|
return;
|
|
104
|
-
const nextGeneration =
|
|
105
|
-
const nextKey = args === undefined ? undefined :
|
|
110
|
+
const nextGeneration = internal.registry.get(internal.generation);
|
|
111
|
+
const nextKey = args === undefined ? undefined : encodeQueryKey(args);
|
|
106
112
|
if (nextKey === key && nextGeneration === generation)
|
|
107
113
|
return;
|
|
108
114
|
const selected = ++revision;
|
|
109
115
|
disconnect();
|
|
110
116
|
key = nextKey;
|
|
111
117
|
generation = nextGeneration;
|
|
112
|
-
const nextAtom = args === undefined ? undefined :
|
|
118
|
+
const nextAtom = args === undefined ? undefined : internal.query(definition, args);
|
|
113
119
|
if (disposed || selected !== revision)
|
|
114
120
|
return;
|
|
115
121
|
atom = nextAtom;
|
|
116
122
|
if (nextAtom) {
|
|
117
123
|
// Subscription setup itself can execute a synchronous Effect and reenter selection.
|
|
118
|
-
const
|
|
124
|
+
const releaseUse = internal.retain(nextAtom);
|
|
125
|
+
const unsubscribe = internal.registry.subscribe(nextAtom, notify);
|
|
126
|
+
const release = () => {
|
|
127
|
+
unsubscribe();
|
|
128
|
+
releaseUse();
|
|
129
|
+
};
|
|
119
130
|
if (disposed || selected !== revision) {
|
|
120
131
|
release();
|
|
121
132
|
return;
|
|
@@ -136,9 +147,10 @@ export function queryResource(context, definition) {
|
|
|
136
147
|
refresh: () => {
|
|
137
148
|
if (atom &&
|
|
138
149
|
!disposed &&
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
!internal.disposed() &&
|
|
151
|
+
generation === internal.registry.get(internal.generation) &&
|
|
152
|
+
!internal.registry.get(atom).waiting)
|
|
153
|
+
internal.refresh(atom);
|
|
142
154
|
},
|
|
143
155
|
dispose: () => {
|
|
144
156
|
if (disposed)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Internal completion accounting, including reentrant acquisition before a fiber is available. */
|
|
2
|
+
export class Settlement {
|
|
3
|
+
pending = 0;
|
|
4
|
+
waiters = [];
|
|
5
|
+
begin() {
|
|
6
|
+
this.pending++;
|
|
7
|
+
let finished = false;
|
|
8
|
+
return () => {
|
|
9
|
+
if (finished)
|
|
10
|
+
return;
|
|
11
|
+
finished = true;
|
|
12
|
+
if (--this.pending)
|
|
13
|
+
return;
|
|
14
|
+
for (const done of this.waiters)
|
|
15
|
+
done();
|
|
16
|
+
this.waiters.length = 0;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
wait() {
|
|
20
|
+
return new Promise((done) => {
|
|
21
|
+
if (this.pending)
|
|
22
|
+
this.waiters.push(done);
|
|
23
|
+
else
|
|
24
|
+
done();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
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,96 @@
|
|
|
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 changed;
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
const custom = fields && Object.hasOwn(fields, key) ? fields[key] : undefined;
|
|
66
|
+
const item = custom && Object.hasOwn(old, key)
|
|
67
|
+
? custom(old[key], value[key])
|
|
68
|
+
: share(Object.hasOwn(old, key) ? old[key] : undefined, value[key], undefined);
|
|
69
|
+
if (!Object.hasOwn(old, key) || !Object.is(item, old[key]))
|
|
70
|
+
equal = false;
|
|
71
|
+
if (!Object.is(item, value[key]))
|
|
72
|
+
(changed ??= new Map()).set(key, item);
|
|
73
|
+
}
|
|
74
|
+
let result = equal ? previous : next;
|
|
75
|
+
if (!equal && changed) {
|
|
76
|
+
const shared = array ? [] : {};
|
|
77
|
+
if (array)
|
|
78
|
+
shared.length = value.length;
|
|
79
|
+
for (const key of keys)
|
|
80
|
+
Object.defineProperty(shared, key, {
|
|
81
|
+
value: changed.has(key) ? changed.get(key) : value[key],
|
|
82
|
+
writable: true,
|
|
83
|
+
enumerable: true,
|
|
84
|
+
configurable: true,
|
|
85
|
+
});
|
|
86
|
+
result = shared;
|
|
87
|
+
}
|
|
88
|
+
if (fields)
|
|
89
|
+
return result;
|
|
90
|
+
if (!pairs) {
|
|
91
|
+
pairs = new WeakMap();
|
|
92
|
+
sharedPairs.set(previous, pairs);
|
|
93
|
+
}
|
|
94
|
+
pairs.set(next, result);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const compiledSlots: WeakSet<object>;
|
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,31 +1,38 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
3
|
+
/** Protect published plain data in every build. Opaque objects keep their own lifecycle. */
|
|
4
|
+
export function protectSnapshot(value) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const plain = [];
|
|
7
|
+
const validate = (item) => {
|
|
8
|
+
if (!item || typeof item !== 'object' || checked.has(item) || seen.has(item))
|
|
9
|
+
return;
|
|
10
|
+
seen.add(item);
|
|
11
|
+
// Effect wrappers remain opaque, but their successful UI data is plain data.
|
|
12
|
+
if (isAsyncResult(item)) {
|
|
13
|
+
if (item._tag === 'Success')
|
|
14
|
+
validate(item.value);
|
|
15
|
+
else if (item._tag === 'Failure' && item.previousSuccess._tag === 'Some')
|
|
16
|
+
validate(item.previousSuccess.value);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const prototype = Object.getPrototypeOf(item);
|
|
20
|
+
if (!Array.isArray(item) && prototype !== Object.prototype && prototype !== null)
|
|
21
|
+
return;
|
|
22
|
+
for (const key of Reflect.ownKeys(item)) {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
24
|
+
if (!Object.hasOwn(descriptor, 'value'))
|
|
25
|
+
throw new Error(`Snapshot data cannot contain an accessor (${String(key)}). Materialize its value before publication; keep services in opaque instances.`);
|
|
26
|
+
validate(descriptor.value);
|
|
27
|
+
}
|
|
28
|
+
plain.push(item);
|
|
29
|
+
};
|
|
30
|
+
// Validate the complete graph before caching any proof. A rejected cyclic graph
|
|
31
|
+
// must not make a later publication skip validation through a previously seen child.
|
|
32
|
+
validate(value);
|
|
33
|
+
for (const item of plain)
|
|
34
|
+
Object.freeze(item);
|
|
35
|
+
for (const item of seen)
|
|
36
|
+
checked.add(item);
|
|
30
37
|
return value;
|
|
31
38
|
}
|
|
@@ -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;
|