effectweb 0.2.1 → 0.2.3
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 +1 -1
- package/dist/AsyncContent.js +30 -21
- package/dist/actions.d.ts +0 -1
- package/dist/actions.js +0 -1
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js +3 -4
- package/dist/collection.d.ts +4 -1
- package/dist/collection.js +44 -1
- package/dist/dom.d.ts +3 -1
- package/dist/dom.js +81 -4
- package/dist/load.d.ts +0 -1
- package/dist/owner.js +0 -2
- package/dist/pages.d.ts +0 -1
- package/dist/pages.js +0 -1
- package/dist/program.d.ts +0 -1
- package/dist/program.js +29 -19
- package/dist/query.d.ts +4 -1
- package/dist/session.d.ts +0 -3
- package/dist/session.js +0 -3
- package/dist/share.d.ts +5 -1
- package/dist/share.js +9 -3
- package/dist/state.d.ts +0 -1
- package/dist/state.js +0 -1
- package/dist/testing.d.ts +0 -1
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -4,6 +4,6 @@ Immutable Effect models and JSX compiled to direct DOM updates, without signals,
|
|
|
4
4
|
|
|
5
5
|
Use with `@effectweb/compiler/vite` and Effect `4.0.0-rc.112`. The package includes programs, named tasks, async presentation, query caching, DOM lifetimes, and `effectweb/testing` helpers.
|
|
6
6
|
|
|
7
|
-
See [setup and example](https://github.com/DerpyCrabs/EffectWeb#vite-setup)
|
|
7
|
+
See [setup and example](https://github.com/DerpyCrabs/EffectWeb#vite-setup).
|
|
8
8
|
|
|
9
9
|
Early API; runtime and compiler versions advance together. Client-side only. Persistence and multi-tab coordination belong to the application.
|
package/dist/AsyncContent.js
CHANGED
|
@@ -45,27 +45,36 @@ const implementation = component({
|
|
|
45
45
|
visible: true
|
|
46
46
|
} }),
|
|
47
47
|
view: /* @__PURE__ */ _ew_dom.compiled((_ew_scope_1, _ew_parent_2, _ew_before_3) => {
|
|
48
|
-
const _ew_derived_4 = _ew_scope_1.derive(() => [_ew_scope_1.value?.props], () =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
_ew_dom.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
const _ew_derived_4 = _ew_scope_1.derive(() => [_ew_scope_1.value?.props], () => {
|
|
49
|
+
const _ew_capture_6 = _ew_scope_1.value;
|
|
50
|
+
return _ew_capture_6.props;
|
|
51
|
+
});
|
|
52
|
+
const _ew_derived_7 = _ew_scope_1.derive(() => [_ew_derived_4()?.result], () => {
|
|
53
|
+
const _ew_capture_9 = _ew_derived_4();
|
|
54
|
+
return AsyncResult.value(_ew_capture_9.result);
|
|
55
|
+
});
|
|
56
|
+
const _ew_derived_10 = _ew_scope_1.derive(() => [_ew_derived_4()?.result, _ew_derived_4()?.result?.waiting], () => {
|
|
57
|
+
const _ew_capture_12 = _ew_derived_4();
|
|
58
|
+
return AsyncResult.isFailure(_ew_capture_12.result) && !_ew_capture_12.result.waiting ? _ew_capture_12.result : undefined;
|
|
59
|
+
});
|
|
60
|
+
_ew_dom.branch(_ew_scope_1, _ew_parent_2, _ew_before_3, () => Option.isSome(_ew_derived_7()), (_ew_scope_1, _ew_parent_13, _ew_before_14) => {
|
|
61
|
+
_ew_dom.text(_ew_scope_1, _ew_parent_13, _ew_before_14, () => [_ew_derived_4(), _ew_derived_7()?.value], () => _ew_derived_4().content(_ew_derived_7().value));
|
|
62
|
+
_ew_dom.branch(_ew_scope_1, _ew_parent_13, _ew_before_14, () => _ew_derived_4().result.waiting, (_ew_scope_1, _ew_parent_15, _ew_before_16) => {
|
|
63
|
+
_ew_dom.text(_ew_scope_1, _ew_parent_15, _ew_before_16, () => [_ew_derived_4()?.refreshing], () => _ew_derived_4().refreshing);
|
|
64
|
+
}, (_ew_scope_1, _ew_parent_15, _ew_before_16) => { });
|
|
65
|
+
_ew_dom.branch(_ew_scope_1, _ew_parent_13, _ew_before_14, () => _ew_derived_10() && _ew_derived_4().failure, (_ew_scope_1, _ew_parent_17, _ew_before_18) => {
|
|
66
|
+
_ew_dom.text(_ew_scope_1, _ew_parent_17, _ew_before_18, () => [_ew_derived_4(), _ew_derived_10()?.cause], () => _ew_derived_4().failure(_ew_derived_10().cause));
|
|
67
|
+
}, (_ew_scope_1, _ew_parent_17, _ew_before_18) => { });
|
|
68
|
+
}, (_ew_scope_1, _ew_parent_13, _ew_before_14) => {
|
|
69
|
+
_ew_dom.branch(_ew_scope_1, _ew_parent_13, _ew_before_14, () => _ew_derived_10() && _ew_derived_4().failure, (_ew_scope_1, _ew_parent_19, _ew_before_20) => {
|
|
70
|
+
_ew_dom.text(_ew_scope_1, _ew_parent_19, _ew_before_20, () => [_ew_derived_4(), _ew_derived_10()?.cause], () => _ew_derived_4().failure(_ew_derived_10().cause));
|
|
71
|
+
}, (_ew_scope_1, _ew_parent_19, _ew_before_20) => {
|
|
72
|
+
_ew_dom.branch(_ew_scope_1, _ew_parent_19, _ew_before_20, () => _ew_scope_1.value.pending, (_ew_scope_1, _ew_parent_21, _ew_before_22) => {
|
|
73
|
+
_ew_dom.branch(_ew_scope_1, _ew_parent_21, _ew_before_22, () => _ew_scope_1.value.visible, (_ew_scope_1, _ew_parent_23, _ew_before_24) => {
|
|
74
|
+
_ew_dom.text(_ew_scope_1, _ew_parent_23, _ew_before_24, () => [_ew_derived_4()?.pending], () => _ew_derived_4().pending);
|
|
75
|
+
}, (_ew_scope_1, _ew_parent_23, _ew_before_24) => { });
|
|
76
|
+
}, (_ew_scope_1, _ew_parent_21, _ew_before_22) => {
|
|
77
|
+
_ew_dom.text(_ew_scope_1, _ew_parent_21, _ew_before_22, () => [_ew_derived_4()?.empty], () => _ew_derived_4().empty);
|
|
69
78
|
});
|
|
70
79
|
});
|
|
71
80
|
});
|
package/dist/actions.d.ts
CHANGED
|
@@ -18,7 +18,6 @@ type Creators<Handlers> = {
|
|
|
18
18
|
type Dispatch<Handlers> = {
|
|
19
19
|
[Name in keyof Handlers]: (...args: Arguments<Handlers[Name]>) => void;
|
|
20
20
|
};
|
|
21
|
-
/** Declare action names and payloads once. Dispatch still crosses the ordinary program message queue. */
|
|
22
21
|
export declare function defineActions<Model>(): <Handlers extends Record<string, Handler<Model>>>(handlers: Handlers) => {
|
|
23
22
|
message: Creators<Handlers>;
|
|
24
23
|
bind(this: void, send: Send<HandlerMessage<Handlers>>): Dispatch<Handlers>;
|
package/dist/actions.js
CHANGED
package/dist/cache.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ declare function createQueryCache<R>(runtime?: UiRuntime<R>): {
|
|
|
13
13
|
generation: Atom.Writable<number, number>;
|
|
14
14
|
resource<A, E = unknown>(key: string, load: () => Effect.Effect<A, E>): Atom.Atom<AsyncResult.AsyncResult<A, E>>;
|
|
15
15
|
query: <Args, A, E>(definition: Query<Args, A, E, R>, args: Args) => Atom.Atom<AsyncResult.AsyncResult<A, E>>;
|
|
16
|
-
/** Prefetch and views observe the same atom; failure types and shared cancellation remain intact. */
|
|
17
16
|
prefetch<Args, A, E>(definition: Query<Args, A, E, R>, args: Args): Effect.Effect<A, E>;
|
|
18
17
|
invalidateQuery<Args, A, E>(definition: Query<Args, A, E, R>, ...selected: [] | [Args]): void;
|
|
19
18
|
invalidate(prefix: string): void;
|
package/dist/cache.js
CHANGED
|
@@ -13,7 +13,7 @@ function createQueryCache(runtime) {
|
|
|
13
13
|
const registry = AtomRegistry.make({ defaultIdleTTL: 30_000 });
|
|
14
14
|
const generation = Atom.keepAlive(Atom.make(0));
|
|
15
15
|
const resources = new Map();
|
|
16
|
-
const acquire = (key, load) => {
|
|
16
|
+
const acquire = (key, load, share = shareValue) => {
|
|
17
17
|
let entry = resources.get(key);
|
|
18
18
|
if (entry)
|
|
19
19
|
entry.load = load;
|
|
@@ -24,7 +24,7 @@ function createQueryCache(runtime) {
|
|
|
24
24
|
const previous = Option.flatMap(get.self(), AsyncResult.value);
|
|
25
25
|
return loadEffect(() => next.load()).pipe(Effect.map((value) => {
|
|
26
26
|
next.loadedAt = Date.now();
|
|
27
|
-
return Option.isSome(previous) ?
|
|
27
|
+
return Option.isSome(previous) ? share(previous.value, value) : value;
|
|
28
28
|
}));
|
|
29
29
|
}),
|
|
30
30
|
};
|
|
@@ -47,7 +47,7 @@ function createQueryCache(runtime) {
|
|
|
47
47
|
const { entry, atom } = acquire(queryKey(definition, args), () => {
|
|
48
48
|
const effect = Effect.suspend(() => definition.load(args));
|
|
49
49
|
return runtime ? runtime.provide(effect) : effect;
|
|
50
|
-
});
|
|
50
|
+
}, definition.share);
|
|
51
51
|
entry.queryId = definition.id;
|
|
52
52
|
if (registry.getNodes().has(atom) &&
|
|
53
53
|
entry.loadedAt !== undefined &&
|
|
@@ -65,7 +65,6 @@ function createQueryCache(runtime) {
|
|
|
65
65
|
return acquire(key, load).atom;
|
|
66
66
|
},
|
|
67
67
|
query: selectQuery,
|
|
68
|
-
/** Prefetch and views observe the same atom; failure types and shared cancellation remain intact. */
|
|
69
68
|
prefetch(definition, args) {
|
|
70
69
|
return Effect.suspend(() => AtomRegistry.getResult(registry, selectQuery(definition, args), { suspendOnWaiting: true }));
|
|
71
70
|
},
|
package/dist/collection.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export type Identity = string | number;
|
|
2
|
-
/** Identity belongs to the domain collection, not to each place that renders it. */
|
|
3
2
|
export interface Rows<A> {
|
|
4
3
|
readonly items: readonly A[];
|
|
5
4
|
readonly identity: (item: A, index: number) => Identity;
|
|
@@ -10,6 +9,10 @@ export interface Rows<A> {
|
|
|
10
9
|
}
|
|
11
10
|
export declare function collection<A>(identity: (item: A, index: number) => Identity): {
|
|
12
11
|
from: (items: readonly A[]) => Rows<A>;
|
|
12
|
+
share: {
|
|
13
|
+
<B extends A>(previous: readonly B[], next: B[]): B[];
|
|
14
|
+
<B extends A>(previous: readonly B[], next: readonly B[]): readonly B[];
|
|
15
|
+
};
|
|
13
16
|
};
|
|
14
17
|
/** Use positional identity for ordered values without stable entity IDs. */
|
|
15
18
|
export declare function sequence<A>(items: readonly A[]): Rows<A>;
|
package/dist/collection.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { shareValue } from './share.js';
|
|
1
2
|
export function collection(identity) {
|
|
2
3
|
const cache = new WeakMap();
|
|
4
|
+
const comparisons = new WeakMap();
|
|
3
5
|
const from = (items) => {
|
|
4
6
|
const cached = cache.get(items);
|
|
5
7
|
if (cached)
|
|
@@ -15,7 +17,48 @@ export function collection(identity) {
|
|
|
15
17
|
cache.set(items, rows);
|
|
16
18
|
return rows;
|
|
17
19
|
};
|
|
18
|
-
|
|
20
|
+
function share(previous, next) {
|
|
21
|
+
if (previous === next)
|
|
22
|
+
return previous;
|
|
23
|
+
let pairs = comparisons.get(previous);
|
|
24
|
+
const cached = pairs?.get(next);
|
|
25
|
+
if (cached)
|
|
26
|
+
return cached;
|
|
27
|
+
let byIdentity;
|
|
28
|
+
let result;
|
|
29
|
+
let equal = previous.length === next.length;
|
|
30
|
+
for (let index = 0; index < next.length; index++) {
|
|
31
|
+
const item = next[index];
|
|
32
|
+
if (index < previous.length && Object.is(previous[index], item))
|
|
33
|
+
continue;
|
|
34
|
+
const key = identity(item, index);
|
|
35
|
+
let old = previous[index];
|
|
36
|
+
if (index >= previous.length || !Object.is(identity(old, index), key)) {
|
|
37
|
+
if (!byIdentity) {
|
|
38
|
+
byIdentity = new Map(previous.map((value, i) => [identity(value, i), value]));
|
|
39
|
+
const keys = next.map(identity);
|
|
40
|
+
if (byIdentity.size !== previous.length || new Set(keys).size !== keys.length)
|
|
41
|
+
throw new Error('Duplicate collection identity. Identity must be unique within the collection.');
|
|
42
|
+
}
|
|
43
|
+
old = byIdentity.get(key);
|
|
44
|
+
}
|
|
45
|
+
const value = old === undefined ? item : shareValue(old, item);
|
|
46
|
+
if (!Object.is(value, previous[index]))
|
|
47
|
+
equal = false;
|
|
48
|
+
if (!Object.is(value, item) && !result)
|
|
49
|
+
result = next.slice();
|
|
50
|
+
if (result)
|
|
51
|
+
result[index] = value;
|
|
52
|
+
}
|
|
53
|
+
const shared = equal ? previous : (result ?? next);
|
|
54
|
+
if (!pairs) {
|
|
55
|
+
pairs = new WeakMap();
|
|
56
|
+
comparisons.set(previous, pairs);
|
|
57
|
+
}
|
|
58
|
+
pairs.set(next, shared);
|
|
59
|
+
return shared;
|
|
60
|
+
}
|
|
61
|
+
return { from, share };
|
|
19
62
|
}
|
|
20
63
|
const positions = collection((_item, index) => index);
|
|
21
64
|
/** Use positional identity for ordered values without stable entity IDs. */
|
package/dist/dom.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ export declare function mountView<M, E>(parent: Node, definition: View<M, E>, so
|
|
|
52
52
|
}): () => void;
|
|
53
53
|
export declare function element(parent: Node, before: Node | null, tag: string): Element;
|
|
54
54
|
export declare function attribute(element: Element, name: string, value: unknown): void;
|
|
55
|
+
/** Compiler output for attributes whose unchanged value needs no DOM refresh. */
|
|
56
|
+
export declare function bindAttribute<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
|
|
55
57
|
export declare function text<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
|
|
56
58
|
export declare function event<M, E>(scope: Scope<M, E>, element: Element, name: string, handler: (event: Event) => unknown): void;
|
|
57
59
|
export declare function branch<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, choose: () => boolean, yes: Build<M, E>, no: Build<M, E>): void;
|
|
@@ -61,6 +63,6 @@ export declare function child<M, E, C, F>(scope: Scope<M, E>, parent: Node, befo
|
|
|
61
63
|
export declare function attach<M, E>(scope: Scope<M, E>, element: Element, dependencies: Dependencies, read: () => DomMount | undefined): void;
|
|
62
64
|
export declare function portal<M, E>(scope: Scope<M, E>, build: Build<M, E>): void;
|
|
63
65
|
/** Build static markup once per document/namespace, then clone native nodes at each use. */
|
|
64
|
-
export declare function template(build: (parent: Node, before: Node | null) => void): (parent: Node, before: Node | null) =>
|
|
66
|
+
export declare function template(build: string | ((parent: Node, before: Node | null) => void), depth?: number): (parent: Node, before: Node | null) => Node;
|
|
65
67
|
export declare function literal(parent: Node, before: Node | null, value: string): void;
|
|
66
68
|
export {};
|
package/dist/dom.js
CHANGED
|
@@ -325,6 +325,29 @@ export function attribute(element, name, value) {
|
|
|
325
325
|
if (name === 'style' && (value == null || typeof value !== 'object'))
|
|
326
326
|
styleProperties.delete(element);
|
|
327
327
|
}
|
|
328
|
+
/** Compiler output for attributes whose unchanged value needs no DOM refresh. */
|
|
329
|
+
export function bindAttribute(scope, element, name, dependencies, read, source) {
|
|
330
|
+
let previous = dependencies();
|
|
331
|
+
let value = read();
|
|
332
|
+
attribute(element, name, value);
|
|
333
|
+
traceBinding(source, undefined, previous, 'binding');
|
|
334
|
+
if (!previous.length)
|
|
335
|
+
return;
|
|
336
|
+
// Keep the value cache in the dependency job itself, without an extra closure
|
|
337
|
+
// for every row. Controlled input properties keep using ordinary watches.
|
|
338
|
+
scope.jobs.push(() => {
|
|
339
|
+
const next = dependencies();
|
|
340
|
+
if (equal(previous, next))
|
|
341
|
+
return;
|
|
342
|
+
const nextValue = read();
|
|
343
|
+
if (!Object.is(value, nextValue)) {
|
|
344
|
+
attribute(element, name, nextValue);
|
|
345
|
+
value = nextValue;
|
|
346
|
+
}
|
|
347
|
+
traceBinding(source, previous, next, 'binding');
|
|
348
|
+
previous = next;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
328
351
|
export function text(scope, parent, before, dependencies, read, source) {
|
|
329
352
|
const node = document.createTextNode('');
|
|
330
353
|
parent.insertBefore(node, before);
|
|
@@ -430,10 +453,31 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
430
453
|
const next = read();
|
|
431
454
|
const nextOuter = outer();
|
|
432
455
|
const outerChanged = !equal(previousOuter, nextOuter);
|
|
433
|
-
if (next === previousList
|
|
456
|
+
if (next === previousList) {
|
|
457
|
+
if (outerChanged)
|
|
458
|
+
for (const row of rows.values())
|
|
459
|
+
row.scope.set(row.scope.value);
|
|
460
|
+
previousOuter = nextOuter;
|
|
434
461
|
return;
|
|
462
|
+
}
|
|
435
463
|
const collection = 'items' in next ? next : undefined;
|
|
436
464
|
const items = collection ? collection.items : next;
|
|
465
|
+
// A list filling its container can clear the DOM in one operation. Root lists
|
|
466
|
+
// and lists with adjacent content retain their bounded per-row removal path.
|
|
467
|
+
if (!items.length &&
|
|
468
|
+
rows.size &&
|
|
469
|
+
target instanceof Element &&
|
|
470
|
+
target.firstChild === rows.get(previousIdentities[0])?.start &&
|
|
471
|
+
target.lastChild === end) {
|
|
472
|
+
for (const row of rows.values())
|
|
473
|
+
row.scope.dispose();
|
|
474
|
+
target.replaceChildren(end);
|
|
475
|
+
rows.clear();
|
|
476
|
+
previousList = next;
|
|
477
|
+
previousOuter = nextOuter;
|
|
478
|
+
previousIdentities = [];
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
437
481
|
const identities = items.map((item, index) => {
|
|
438
482
|
const key = collection ? collection.identity(item, index) : item;
|
|
439
483
|
if (typeof key !== 'string' && typeof key !== 'number')
|
|
@@ -593,8 +637,30 @@ export function portal(scope, build) {
|
|
|
593
637
|
build(child, host, null);
|
|
594
638
|
scope.jobs.push(() => child.set(scope.value));
|
|
595
639
|
}
|
|
640
|
+
// HTML parsing supplies the initial nodes. Unusual placements, such as mounting
|
|
641
|
+
// an HTML-named view inside SVG, still follow the same namespace rules as element().
|
|
642
|
+
function placeTemplate(parent, node) {
|
|
643
|
+
if (node.nodeType !== 1) {
|
|
644
|
+
parent.appendChild(node);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const source = node;
|
|
648
|
+
const namespace = source.localName === 'svg' || svgChildren(parent)
|
|
649
|
+
? svgNamespace
|
|
650
|
+
: 'http://www.w3.org/1999/xhtml';
|
|
651
|
+
if (source.namespaceURI === namespace) {
|
|
652
|
+
parent.appendChild(source);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const target = element(parent, null, source.localName);
|
|
656
|
+
for (const attr of source.attributes)
|
|
657
|
+
target.setAttribute(attr.name, attr.value);
|
|
658
|
+
while (source.firstChild)
|
|
659
|
+
placeTemplate(target, source.firstChild);
|
|
660
|
+
source.remove();
|
|
661
|
+
}
|
|
596
662
|
/** Build static markup once per document/namespace, then clone native nodes at each use. */
|
|
597
|
-
export function template(build) {
|
|
663
|
+
export function template(build, depth = 0) {
|
|
598
664
|
const documents = new WeakMap();
|
|
599
665
|
return (parent, before) => {
|
|
600
666
|
const doc = parent.ownerDocument ?? document;
|
|
@@ -609,7 +675,16 @@ export function template(build) {
|
|
|
609
675
|
const host = svg
|
|
610
676
|
? doc.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
611
677
|
: doc.createElement('div');
|
|
612
|
-
|
|
678
|
+
if (typeof build === 'string') {
|
|
679
|
+
const source = doc.createElement('template');
|
|
680
|
+
source.innerHTML = build;
|
|
681
|
+
let node = source.content.firstChild;
|
|
682
|
+
for (let index = 0; index < depth; index++)
|
|
683
|
+
node = node.firstChild;
|
|
684
|
+
placeTemplate(host, node);
|
|
685
|
+
}
|
|
686
|
+
else
|
|
687
|
+
build(host, null);
|
|
613
688
|
if (host.firstChild && !host.firstChild.nextSibling) {
|
|
614
689
|
fragment = host.removeChild(host.firstChild);
|
|
615
690
|
}
|
|
@@ -620,7 +695,9 @@ export function template(build) {
|
|
|
620
695
|
}
|
|
621
696
|
variants.set(svg, fragment);
|
|
622
697
|
}
|
|
623
|
-
|
|
698
|
+
const clone = fragment.cloneNode(true);
|
|
699
|
+
parent.insertBefore(clone, before);
|
|
700
|
+
return clone;
|
|
624
701
|
};
|
|
625
702
|
}
|
|
626
703
|
export function literal(parent, before, value) {
|
package/dist/load.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
|
-
/** Loaders stay lazy and use one error/cancellation model, whether their work is sync or async. */
|
|
3
2
|
export type UiLoad<A, E = unknown, R = never> = Effect.Effect<A, E, R>;
|
|
4
3
|
export declare const loadEffect: <A, E, R>(load: () => UiLoad<A, E, R>) => UiLoad<A, E, R>;
|
|
5
4
|
/** Adapt Promise libraries at the integration boundary. Pass the signal to APIs that support it. */
|
package/dist/owner.js
CHANGED
|
@@ -3,7 +3,6 @@ import { Effect } from 'effect';
|
|
|
3
3
|
import { program } from './program.js';
|
|
4
4
|
import { patchModel } from './state.js';
|
|
5
5
|
import { runAll, reportError } from './errors.js';
|
|
6
|
-
/** Immutable model publication and task ownership, using the same program queue as components. */
|
|
7
6
|
export function modelOwner(initial, options = {}) {
|
|
8
7
|
let disposed = false, nextTask = 0;
|
|
9
8
|
let staged;
|
|
@@ -85,7 +84,6 @@ export function modelOwner(initial, options = {}) {
|
|
|
85
84
|
edit: (key, change) => {
|
|
86
85
|
if (!disposed) {
|
|
87
86
|
const changes = {};
|
|
88
|
-
// Readonly changes remain immutable model data; no clone is needed for a no-op.
|
|
89
87
|
changes[key] = change(read()[key]);
|
|
90
88
|
patch(changes);
|
|
91
89
|
}
|
package/dist/pages.d.ts
CHANGED
|
@@ -32,7 +32,6 @@ export declare function pages<Props, A, Cursor>(definition: {
|
|
|
32
32
|
load: (props: Props, cursor: Cursor | undefined) => UiLoad<UiPage<A, Cursor>>;
|
|
33
33
|
itemKey: (item: A) => string;
|
|
34
34
|
}): {
|
|
35
|
-
/** Own pagination directly in a controller; components can use the same reducer. */
|
|
36
35
|
create(props: Props): {
|
|
37
36
|
send: Send<PagesMessage<A, Cursor>>;
|
|
38
37
|
receive: (props: Props) => void;
|
package/dist/pages.js
CHANGED
package/dist/program.d.ts
CHANGED
|
@@ -40,7 +40,6 @@ export interface RunningProgram<Model, Message> extends Program<Model, Message>
|
|
|
40
40
|
readonly activeSlots: () => readonly string[];
|
|
41
41
|
readonly awaitIdle: (slot?: string) => Promise<void>;
|
|
42
42
|
}
|
|
43
|
-
/** One immutable source of truth. Reducers are synchronous; commands own async work. */
|
|
44
43
|
export declare function program<Model, Message>(options: {
|
|
45
44
|
initial: Model;
|
|
46
45
|
name?: string;
|
package/dist/program.js
CHANGED
|
@@ -2,8 +2,6 @@ import { protectSnapshot, checkSnapshotsByDefault } from './snapshot.js';
|
|
|
2
2
|
import { reportError, reportSafely } from './errors.js';
|
|
3
3
|
import { traceProgram, nextProgramId, hasProgramObservers } from './diagnostics.js';
|
|
4
4
|
import { Cause, Effect, Fiber, Option, Stream } from 'effect';
|
|
5
|
-
import * as Atom from 'effect/unstable/reactivity/Atom';
|
|
6
|
-
import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
|
|
7
5
|
/** A lazy request settles successes, typed failures and defects through one message contract. */
|
|
8
6
|
export function effectCommand(slot, load, handlers) {
|
|
9
7
|
return { slot, effect: Effect.suspend(load).pipe(Effect.matchCause(handlers)) };
|
|
@@ -19,7 +17,6 @@ export function mapCommand(command, map) {
|
|
|
19
17
|
? { slot: command.slot, stream: command.stream.pipe(Stream.map(map)) }
|
|
20
18
|
: { slot: command.slot, effect: command.effect.pipe(Effect.map(map)) };
|
|
21
19
|
}
|
|
22
|
-
/** One immutable source of truth. Reducers are synchronous; commands own async work. */
|
|
23
20
|
export function program(options) {
|
|
24
21
|
const checkSnapshots = options.checkSnapshots ?? checkSnapshotsByDefault;
|
|
25
22
|
const id = nextProgramId();
|
|
@@ -46,9 +43,10 @@ export function program(options) {
|
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
45
|
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// A program publishes one immutable value. It needs no reactive dependency graph;
|
|
47
|
+
// Effect still owns all command fibers, streams, and cancellation below.
|
|
48
|
+
let current = protectSnapshot(options.initial, checkSnapshots);
|
|
49
|
+
const listeners = new Set();
|
|
52
50
|
const running = new Map();
|
|
53
51
|
let disposed = false;
|
|
54
52
|
let draining = false;
|
|
@@ -71,11 +69,16 @@ export function program(options) {
|
|
|
71
69
|
try {
|
|
72
70
|
while (queue.length && !disposed) {
|
|
73
71
|
const message = queue.shift();
|
|
74
|
-
const transition = options.update(
|
|
72
|
+
const transition = options.update(current, message);
|
|
75
73
|
trace('update', undefined, message);
|
|
76
74
|
for (const slot of transition.cancel ?? [])
|
|
77
75
|
cancel(slot);
|
|
78
|
-
|
|
76
|
+
const next = protectSnapshot(transition.model, checkSnapshots);
|
|
77
|
+
if (!Object.is(current, next)) {
|
|
78
|
+
current = next;
|
|
79
|
+
for (const listener of listeners)
|
|
80
|
+
listener(current);
|
|
81
|
+
}
|
|
79
82
|
for (const command of transition.commands ?? []) {
|
|
80
83
|
if (disposed)
|
|
81
84
|
break;
|
|
@@ -124,7 +127,7 @@ export function program(options) {
|
|
|
124
127
|
}
|
|
125
128
|
};
|
|
126
129
|
return {
|
|
127
|
-
model: () =>
|
|
130
|
+
model: () => current,
|
|
128
131
|
activeSlots: () => [...running.keys()],
|
|
129
132
|
awaitIdle: (slot) => disposed || (!draining && (slot ? !running.has(slot) : running.size === 0))
|
|
130
133
|
? Promise.resolve()
|
|
@@ -132,14 +135,22 @@ export function program(options) {
|
|
|
132
135
|
waiters.add({ slot, done });
|
|
133
136
|
}),
|
|
134
137
|
send,
|
|
135
|
-
subscribe
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
subscribe(listener) {
|
|
139
|
+
if (disposed)
|
|
140
|
+
return () => { };
|
|
141
|
+
const receive = (model) => {
|
|
142
|
+
try {
|
|
143
|
+
listener(model);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
reportSafely(options.onDefect ?? reportError, error);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
listeners.add(receive);
|
|
150
|
+
return () => {
|
|
151
|
+
listeners.delete(receive);
|
|
152
|
+
};
|
|
153
|
+
},
|
|
143
154
|
dispose() {
|
|
144
155
|
if (disposed)
|
|
145
156
|
return;
|
|
@@ -147,8 +158,7 @@ export function program(options) {
|
|
|
147
158
|
queue.length = 0;
|
|
148
159
|
for (const slot of running.keys())
|
|
149
160
|
cancel(slot);
|
|
150
|
-
|
|
151
|
-
registry.dispose();
|
|
161
|
+
listeners.clear();
|
|
152
162
|
trace('dispose');
|
|
153
163
|
notify();
|
|
154
164
|
},
|
package/dist/query.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Effect } from 'effect';
|
|
2
|
-
/** One logical read. Share its definition between consumers, prefetching and mutations. */
|
|
3
2
|
export interface Query<Args, A, E = never, R = never> {
|
|
4
3
|
readonly id: number;
|
|
5
4
|
readonly name: string;
|
|
@@ -7,16 +6,20 @@ export interface Query<Args, A, E = never, R = never> {
|
|
|
7
6
|
readonly load: (args: Args) => Effect.Effect<A, E, R>;
|
|
8
7
|
/** Revalidate on activation/prefetch after this many milliseconds. No polling timer. */
|
|
9
8
|
readonly staleTime: number;
|
|
9
|
+
/** Reuse unchanged domain entities before publishing a refreshed result. */
|
|
10
|
+
readonly share?: (previous: A, next: A) => A;
|
|
10
11
|
}
|
|
11
12
|
/** Identity belongs to arguments, never callback identity. Separate caches isolate independent data scopes. */
|
|
12
13
|
export declare function query<A, E = never, R = never>(definition: {
|
|
13
14
|
readonly name: string;
|
|
14
15
|
readonly load: () => Effect.Effect<A, E, R>;
|
|
15
16
|
readonly staleTime?: number;
|
|
17
|
+
readonly share?: (previous: A, next: A) => A;
|
|
16
18
|
}): Query<true, A, E, R>;
|
|
17
19
|
export declare function query<Args, A, E = never, R = never>(definition: {
|
|
18
20
|
readonly name: string;
|
|
19
21
|
readonly key: (args: Args) => string;
|
|
20
22
|
readonly load: (args: Args) => Effect.Effect<A, E, R>;
|
|
21
23
|
readonly staleTime?: number;
|
|
24
|
+
readonly share?: (previous: A, next: A) => A;
|
|
22
25
|
}): Query<Args, A, E, R>;
|
package/dist/session.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { Query } from './query.js';
|
|
|
2
2
|
import type { DisposableOwner } from './owner.js';
|
|
3
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
4
4
|
import type { QueryCache } from './cache.js';
|
|
5
|
-
/** Own subscriptions, DOM adapters and other cleanup for one lifetime. */
|
|
6
5
|
export declare function lifetime(): {
|
|
7
6
|
add(cleanup: () => void): void;
|
|
8
7
|
dispose(): void;
|
|
@@ -27,7 +26,6 @@ export declare function queryResource<Args, A, E, R>(context: {
|
|
|
27
26
|
refresh: () => void;
|
|
28
27
|
dispose: () => void;
|
|
29
28
|
};
|
|
30
|
-
/** Publish Effect results directly; the owner releases the subscription on disposal. */
|
|
31
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): {
|
|
32
30
|
select(args: Args | undefined): void;
|
|
33
31
|
read: () => AsyncResult.AsyncResult<A, E>;
|
|
@@ -48,7 +46,6 @@ interface OwnedSession {
|
|
|
48
46
|
refresh?(): void;
|
|
49
47
|
subscribe?(changed: () => void): () => void;
|
|
50
48
|
}
|
|
51
|
-
/** One declaration owns subscriptions, refresh order and disposal for a group of sessions. */
|
|
52
49
|
export declare function sessionGroup(sessions: readonly OwnedSession[], changed: () => void): {
|
|
53
50
|
refresh: () => void;
|
|
54
51
|
dispose: () => void;
|
package/dist/session.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
2
2
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
3
3
|
import * as Atom from 'effect/unstable/reactivity/Atom';
|
|
4
|
-
/** Own subscriptions, DOM adapters and other cleanup for one lifetime. */
|
|
5
4
|
export function lifetime() {
|
|
6
5
|
const cleanups = [];
|
|
7
6
|
let disposed = false;
|
|
@@ -154,7 +153,6 @@ export function queryResource(context, definition) {
|
|
|
154
153
|
},
|
|
155
154
|
};
|
|
156
155
|
}
|
|
157
|
-
/** Publish Effect results directly; the owner releases the subscription on disposal. */
|
|
158
156
|
export function observeQuery(owner, cache, definition, changed) {
|
|
159
157
|
const resource = queryResource({ cache }, definition);
|
|
160
158
|
resource.subscribe(changed);
|
|
@@ -180,7 +178,6 @@ export function projectionCache() {
|
|
|
180
178
|
select,
|
|
181
179
|
};
|
|
182
180
|
}
|
|
183
|
-
/** One declaration owns subscriptions, refresh order and disposal for a group of sessions. */
|
|
184
181
|
export function sessionGroup(sessions, changed) {
|
|
185
182
|
const scope = lifetime();
|
|
186
183
|
for (const session of sessions) {
|
package/dist/share.d.ts
CHANGED
|
@@ -1,2 +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
|
+
};
|
|
1
5
|
/** Share JSON-shaped data; Blob, typed arrays and class instances retain their own identity. */
|
|
2
|
-
export declare function shareValue<T>(previous: T, next: T): T;
|
|
6
|
+
export declare function shareValue<T>(previous: T, next: T, fields?: ShareFields<T>): T;
|
package/dist/share.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// already compared object pair without retaining either snapshot after its owners release it.
|
|
3
3
|
const sharedPairs = new WeakMap();
|
|
4
4
|
/** Share JSON-shaped data; Blob, typed arrays and class instances retain their own identity. */
|
|
5
|
-
export function shareValue(previous, next) {
|
|
5
|
+
export function shareValue(previous, next, fields) {
|
|
6
6
|
if (Object.is(previous, next))
|
|
7
7
|
return previous;
|
|
8
8
|
if (!previous || !next || typeof previous !== 'object' || typeof next !== 'object')
|
|
@@ -15,7 +15,7 @@ export function shareValue(previous, next) {
|
|
|
15
15
|
Object.getPrototypeOf(previous) !== Object.prototype))
|
|
16
16
|
return next;
|
|
17
17
|
let pairs = sharedPairs.get(previous);
|
|
18
|
-
if (pairs?.has(next))
|
|
18
|
+
if (!fields && pairs?.has(next))
|
|
19
19
|
return pairs.get(next);
|
|
20
20
|
const old = previous;
|
|
21
21
|
const value = next;
|
|
@@ -24,13 +24,19 @@ export function shareValue(previous, next) {
|
|
|
24
24
|
let unchangedNext = true;
|
|
25
25
|
const shared = array ? [] : {};
|
|
26
26
|
for (const key of keys) {
|
|
27
|
-
|
|
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]);
|
|
28
32
|
if (!Object.hasOwn(old, key) || shared[key] !== old[key])
|
|
29
33
|
equal = false;
|
|
30
34
|
if (shared[key] !== value[key])
|
|
31
35
|
unchangedNext = false;
|
|
32
36
|
}
|
|
33
37
|
const result = equal ? previous : unchangedNext ? next : shared;
|
|
38
|
+
if (fields)
|
|
39
|
+
return result;
|
|
34
40
|
if (!pairs) {
|
|
35
41
|
pairs = new WeakMap();
|
|
36
42
|
sharedPairs.set(previous, pairs);
|
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])))
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
2
|
import type { RunningProgram } from './program.js';
|
|
3
3
|
import { type UiRuntime } from './runtime.js';
|
|
4
|
-
/** Drive the real queue. Test services and TestClock effects use the same supplied context. */
|
|
5
4
|
export declare function programDriver<M, Msg>(source: RunningProgram<M, Msg>): ReturnType<typeof makeDriver<M, Msg, never>>;
|
|
6
5
|
export declare function programDriver<M, Msg, R>(source: RunningProgram<M, Msg>, runtime: UiRuntime<R>): ReturnType<typeof makeDriver<M, Msg, R>>;
|
|
7
6
|
declare function makeDriver<M, Msg, R>(source: RunningProgram<M, Msg>, runtime: UiRuntime<R>): {
|