effectweb 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AsyncContent.js +1 -1
- package/dist/boundary.d.ts +11 -0
- package/dist/boundary.js +66 -0
- package/dist/cache-internals.d.ts +4 -0
- package/dist/cache.d.ts +20 -4
- package/dist/cache.js +174 -25
- package/dist/component.d.ts +3 -0
- package/dist/component.js +70 -10
- package/dist/dom.d.ts +12 -4
- package/dist/dom.js +38 -20
- package/dist/effectEvent.d.ts +2 -1
- package/dist/effectEvent.js +4 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/infinite-query.d.ts +42 -0
- package/dist/infinite-query.js +150 -0
- package/dist/intrinsic.d.ts +2 -0
- package/dist/intrinsic.js +120 -0
- package/dist/keyed-tasks.d.ts +29 -0
- package/dist/keyed-tasks.js +97 -0
- package/dist/lazy.js +9 -1
- package/dist/mount.d.ts +3 -1
- package/dist/mount.js +29 -11
- package/dist/owner.d.ts +3 -1
- package/dist/owner.js +39 -3
- package/dist/pages.d.ts +15 -14
- package/dist/pages.js +6 -4
- package/dist/program.d.ts +10 -0
- package/dist/program.js +71 -24
- package/dist/query-internals.d.ts +4 -2
- package/dist/query.d.ts +6 -0
- package/dist/query.js +7 -1
- package/dist/session.js +13 -4
- package/dist/settlement.d.ts +7 -0
- package/dist/settlement.js +27 -0
- package/dist/sharing.js +16 -13
- package/dist/slotIdentity.d.ts +1 -0
- package/dist/slotIdentity.js +2 -0
- package/dist/snapshot.js +32 -24
- package/dist/tasks.js +1 -0
- package/package.json +9 -1
package/dist/dom.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compiledSlots } from './slotIdentity.js';
|
|
1
2
|
import attributeData from './dom-attributes.json' with { type: 'json' };
|
|
2
3
|
import { validateIdentities } from './collection.js';
|
|
3
4
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
@@ -5,6 +6,7 @@ import { eventEffects } from './effectEvent.js';
|
|
|
5
6
|
import { traceBinding } from './diagnostics.js';
|
|
6
7
|
import { shareData } from './sharing.js';
|
|
7
8
|
import { jsxComponent } from './jsx.js';
|
|
9
|
+
import { Settlement } from './settlement.js';
|
|
8
10
|
import { startMount } from './mount.js';
|
|
9
11
|
const equal = (a, b) => a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
|
|
10
12
|
// A model publication reconciles DOM first and controlled dependent properties second.
|
|
@@ -36,14 +38,16 @@ export class Scope {
|
|
|
36
38
|
value;
|
|
37
39
|
send;
|
|
38
40
|
report;
|
|
41
|
+
settlement;
|
|
39
42
|
jobs = [];
|
|
40
43
|
cleanups = [];
|
|
41
44
|
disposed = false;
|
|
42
45
|
revision = 0;
|
|
43
|
-
constructor(value, send, report = reportError) {
|
|
46
|
+
constructor(value, send, report = reportError, settlement = new Settlement()) {
|
|
44
47
|
this.value = value;
|
|
45
48
|
this.send = send;
|
|
46
49
|
this.report = report;
|
|
50
|
+
this.settlement = settlement;
|
|
47
51
|
}
|
|
48
52
|
derive(dependencies, compute, source) {
|
|
49
53
|
let previous;
|
|
@@ -145,10 +149,10 @@ export function compiledSlot(owner, build) {
|
|
|
145
149
|
placements.clear();
|
|
146
150
|
});
|
|
147
151
|
const definition = {
|
|
148
|
-
mount(parent, before, value) {
|
|
152
|
+
mount(parent, before, value, report, settlement) {
|
|
149
153
|
if (owner.disposed)
|
|
150
154
|
throw new Error('Cannot mount content after its declaring view disposed');
|
|
151
|
-
const scope = new Scope(value, owner.send,
|
|
155
|
+
const scope = new Scope(value, owner.send, report, settlement);
|
|
152
156
|
const fragment = buildFragment(parent);
|
|
153
157
|
const range = markers(fragment, null);
|
|
154
158
|
scope.cleanups.push(() => remove(range.start, range.end));
|
|
@@ -173,10 +177,12 @@ export function compiledSlot(owner, build) {
|
|
|
173
177
|
};
|
|
174
178
|
},
|
|
175
179
|
};
|
|
176
|
-
|
|
180
|
+
const slot = Object.assign((value) => new SlotPlacement(definition, value), {
|
|
177
181
|
[contentBrand]: true,
|
|
178
182
|
definition,
|
|
179
183
|
});
|
|
184
|
+
compiledSlots.add(slot);
|
|
185
|
+
return slot;
|
|
180
186
|
}
|
|
181
187
|
/** This declaration is a compiler marker, never a component setup callback. */
|
|
182
188
|
export function view(_render) {
|
|
@@ -268,11 +274,18 @@ export function mountView(parent, definition, source, options = {}) {
|
|
|
268
274
|
remove(start, end);
|
|
269
275
|
throw error;
|
|
270
276
|
}
|
|
271
|
-
|
|
277
|
+
const dispose = () => {
|
|
272
278
|
if (scope.disposed)
|
|
273
279
|
return;
|
|
274
280
|
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
275
281
|
};
|
|
282
|
+
return Object.assign(dispose, {
|
|
283
|
+
dispose,
|
|
284
|
+
close: () => {
|
|
285
|
+
dispose();
|
|
286
|
+
return scope.settlement.wait();
|
|
287
|
+
},
|
|
288
|
+
});
|
|
276
289
|
});
|
|
277
290
|
}
|
|
278
291
|
export function element(parent, before, tag) {
|
|
@@ -525,7 +538,7 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
525
538
|
}
|
|
526
539
|
content = {
|
|
527
540
|
definition: value.definition,
|
|
528
|
-
mounted: value.definition.mount(node.parentNode, node, value.value, scope.report),
|
|
541
|
+
mounted: value.definition.mount(node.parentNode, node, value.value, scope.report, scope.settlement),
|
|
529
542
|
};
|
|
530
543
|
return;
|
|
531
544
|
}
|
|
@@ -557,10 +570,10 @@ function validateContent(value, active = new Set()) {
|
|
|
557
570
|
}
|
|
558
571
|
}
|
|
559
572
|
const arrayContent = {
|
|
560
|
-
mount: (_parent, before, value, report) => mountArray(report, before, value),
|
|
573
|
+
mount: (_parent, before, value, report, settlement) => mountArray(report, before, value, settlement),
|
|
561
574
|
};
|
|
562
575
|
/** Runtime content arrays are positional; domain lists retain the explicit collection contract. */
|
|
563
|
-
function mountArray(report, before, initial) {
|
|
576
|
+
function mountArray(report, before, initial, settlement) {
|
|
564
577
|
const cells = [];
|
|
565
578
|
const disposeCell = (cell) => {
|
|
566
579
|
cell.scope.dispose();
|
|
@@ -578,7 +591,7 @@ function mountArray(report, before, initial) {
|
|
|
578
591
|
cell.scope.set(values[index]);
|
|
579
592
|
continue;
|
|
580
593
|
}
|
|
581
|
-
const scope = new Scope(values[index], unboundSend, report);
|
|
594
|
+
const scope = new Scope(values[index], unboundSend, report, settlement);
|
|
582
595
|
const fragment = buildFragment(before.parentNode);
|
|
583
596
|
const range = markers(fragment, null);
|
|
584
597
|
try {
|
|
@@ -617,7 +630,7 @@ export function event(scope, element, name, handler) {
|
|
|
617
630
|
try {
|
|
618
631
|
const result = handler(event);
|
|
619
632
|
if (result !== null && typeof result === 'object' && !scope.disposed) {
|
|
620
|
-
effects ??= eventEffects(scope.report);
|
|
633
|
+
effects ??= eventEffects(scope.report, scope.settlement);
|
|
621
634
|
effects.accept(result);
|
|
622
635
|
}
|
|
623
636
|
}
|
|
@@ -655,7 +668,7 @@ export function bindEvent(scope, element, name, dependencies, read) {
|
|
|
655
668
|
current = next;
|
|
656
669
|
if (typeof next === 'function') {
|
|
657
670
|
const handler = next;
|
|
658
|
-
owned = new Scope(next, scope.send, scope.report);
|
|
671
|
+
owned = new Scope(next, scope.send, scope.report, scope.settlement);
|
|
659
672
|
event(owned, element, name, handler);
|
|
660
673
|
}
|
|
661
674
|
});
|
|
@@ -699,7 +712,7 @@ export function bindAttributes(scope, element, dependencies, read) {
|
|
|
699
712
|
binding.set(value);
|
|
700
713
|
continue;
|
|
701
714
|
}
|
|
702
|
-
const owned = new Scope(value, scope.send, scope.report);
|
|
715
|
+
const owned = new Scope(value, scope.send, scope.report, scope.settlement);
|
|
703
716
|
bindings.set(name, owned);
|
|
704
717
|
if (name === 'use')
|
|
705
718
|
attach(owned, element, () => [owned.value], () => owned.value);
|
|
@@ -726,7 +739,7 @@ export function branch(scope, parent, before, choose, yes, no) {
|
|
|
726
739
|
if (active !== next) {
|
|
727
740
|
child?.dispose();
|
|
728
741
|
clear(start, end);
|
|
729
|
-
child = new Scope(scope.value, scope.send, scope.report);
|
|
742
|
+
child = new Scope(scope.value, scope.send, scope.report, scope.settlement);
|
|
730
743
|
const fragment = buildFragment(end.parentNode);
|
|
731
744
|
try {
|
|
732
745
|
(next ? yes : no)(child, fragment, null);
|
|
@@ -804,7 +817,7 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
804
817
|
if (!row) {
|
|
805
818
|
const fragment = buildFragment(target);
|
|
806
819
|
const range = markers(fragment, null);
|
|
807
|
-
const child = new Scope([item, index], scope.send, scope.report);
|
|
820
|
+
const child = new Scope([item, index], scope.send, scope.report, scope.settlement);
|
|
808
821
|
try {
|
|
809
822
|
build(child, fragment, range.end);
|
|
810
823
|
}
|
|
@@ -880,7 +893,7 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
880
893
|
update();
|
|
881
894
|
}
|
|
882
895
|
export function invoke(scope, parent, before, dependencies, read, build) {
|
|
883
|
-
const child = new Scope(read(), scope.send, scope.report);
|
|
896
|
+
const child = new Scope(read(), scope.send, scope.report, scope.settlement);
|
|
884
897
|
scope.cleanups.push(() => child.dispose());
|
|
885
898
|
build(child, parent, before);
|
|
886
899
|
scope.watch(dependencies, () => {
|
|
@@ -890,7 +903,7 @@ export function invoke(scope, parent, before, dependencies, read, build) {
|
|
|
890
903
|
});
|
|
891
904
|
}
|
|
892
905
|
export function child(scope, parent, before, definition, dependencies, model, send) {
|
|
893
|
-
const child = new Scope(model(), send, scope.report);
|
|
906
|
+
const child = new Scope(model(), send, scope.report, scope.settlement);
|
|
894
907
|
scope.cleanups.push(() => child.dispose());
|
|
895
908
|
definition.build(child, parent, before);
|
|
896
909
|
scope.watch(dependencies, () => {
|
|
@@ -900,11 +913,12 @@ export function child(scope, parent, before, definition, dependencies, model, se
|
|
|
900
913
|
});
|
|
901
914
|
}
|
|
902
915
|
/** A late-bound compiled definition owns one replaceable DOM region. */
|
|
903
|
-
export function viewRegion(scope, parent, before) {
|
|
916
|
+
export function viewRegion(scope, parent, before, options = {}) {
|
|
904
917
|
const { start, end } = markers(parent, before);
|
|
905
918
|
let active;
|
|
906
919
|
let definition;
|
|
907
|
-
|
|
920
|
+
if (!options.manual)
|
|
921
|
+
scope.jobs.push(() => active?.set(scope.value));
|
|
908
922
|
scope.cleanups.push(() => {
|
|
909
923
|
active?.dispose();
|
|
910
924
|
remove(start, end);
|
|
@@ -925,7 +939,7 @@ export function viewRegion(scope, parent, before) {
|
|
|
925
939
|
clear(start, end);
|
|
926
940
|
if (!next)
|
|
927
941
|
return;
|
|
928
|
-
const child = new Scope(scope.value, scope.send, scope.report);
|
|
942
|
+
const child = new Scope(scope.value, scope.send, options.report ?? scope.report, scope.settlement);
|
|
929
943
|
const fragment = buildFragment(end.parentNode);
|
|
930
944
|
active = child;
|
|
931
945
|
definition = next;
|
|
@@ -965,14 +979,17 @@ export function attach(scope, element, dependencies, read) {
|
|
|
965
979
|
return;
|
|
966
980
|
queueMicrotask(() => {
|
|
967
981
|
if (!scope.disposed && token === generation) {
|
|
982
|
+
const finished = scope.settlement.begin();
|
|
968
983
|
try {
|
|
969
984
|
const acquired = startMount(element, mount, scope.report);
|
|
985
|
+
acquired.closed.then(finished, finished);
|
|
970
986
|
if (scope.disposed || token !== generation)
|
|
971
987
|
acquired.dispose();
|
|
972
988
|
else
|
|
973
989
|
active = acquired;
|
|
974
990
|
}
|
|
975
991
|
catch (error) {
|
|
992
|
+
finished();
|
|
976
993
|
reportSafely(scope.report, error);
|
|
977
994
|
}
|
|
978
995
|
}
|
|
@@ -998,7 +1015,7 @@ export function portal(scope, build, mount = () => undefined) {
|
|
|
998
1015
|
? target.ownerDocument.createElementNS(svgNamespace, 'g')
|
|
999
1016
|
: target.ownerDocument.createElement('div');
|
|
1000
1017
|
host.style.display = 'contents';
|
|
1001
|
-
child = new Scope(scope.value, scope.send, scope.report);
|
|
1018
|
+
child = new Scope(scope.value, scope.send, scope.report, scope.settlement);
|
|
1002
1019
|
target.appendChild(host);
|
|
1003
1020
|
build(child, host, null);
|
|
1004
1021
|
return;
|
|
@@ -1089,3 +1106,4 @@ export function template(build, depth = 0) {
|
|
|
1089
1106
|
export function literal(parent, before, value) {
|
|
1090
1107
|
parent.insertBefore((parent.ownerDocument ?? document).createTextNode(value), before);
|
|
1091
1108
|
}
|
|
1109
|
+
export { intrinsic } from './intrinsic.js';
|
package/dist/effectEvent.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
2
|
import { type ReportError } from './errors.js';
|
|
3
|
+
import type { Settlement } from './settlement.js';
|
|
3
4
|
import { type UiRuntime } from './runtime.js';
|
|
4
5
|
declare const tag: unique symbol;
|
|
5
6
|
export interface EffectEventRequest {
|
|
@@ -11,7 +12,7 @@ export interface EffectEventRequest {
|
|
|
11
12
|
export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E>): (event: EventType) => EffectEventRequest;
|
|
12
13
|
export declare function effectEvent<EventType extends Event, E, R>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E, R>, runtime: UiRuntime<R>): (event: EventType) => EffectEventRequest;
|
|
13
14
|
/** Internal listener owner, allocated only when an event returns a value needing inspection. */
|
|
14
|
-
export declare function eventEffects(report: ReportError): {
|
|
15
|
+
export declare function eventEffects(report: ReportError, settlement?: Settlement): {
|
|
15
16
|
accept(value: unknown): void;
|
|
16
17
|
dispose(): void;
|
|
17
18
|
};
|
package/dist/effectEvent.js
CHANGED
|
@@ -7,7 +7,7 @@ export function effectEvent(policy, load, runtime) {
|
|
|
7
7
|
return (event) => ({ [tag]: true, policy, effect: owner.provide(load(event)) });
|
|
8
8
|
}
|
|
9
9
|
/** Internal listener owner, allocated only when an event returns a value needing inspection. */
|
|
10
|
-
export function eventEffects(report) {
|
|
10
|
+
export function eventEffects(report, settlement) {
|
|
11
11
|
let active;
|
|
12
12
|
let disposed = false;
|
|
13
13
|
const stop = () => {
|
|
@@ -27,8 +27,10 @@ export function eventEffects(report) {
|
|
|
27
27
|
stop();
|
|
28
28
|
const token = {};
|
|
29
29
|
active = token;
|
|
30
|
+
const finished = settlement?.begin();
|
|
30
31
|
try {
|
|
31
32
|
const fiber = Effect.runFork(request.effect);
|
|
33
|
+
fiber.addObserver(() => finished?.());
|
|
32
34
|
token.fiber = fiber;
|
|
33
35
|
if (disposed || active !== token) {
|
|
34
36
|
Effect.runFork(Fiber.interrupt(fiber));
|
|
@@ -43,6 +45,7 @@ export function eventEffects(report) {
|
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
catch (error) {
|
|
48
|
+
finished?.();
|
|
46
49
|
if (active === token)
|
|
47
50
|
active = undefined;
|
|
48
51
|
reportSafely(report, error);
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { lazyView, type LazyViewOptions } from './lazy.js';
|
|
|
11
11
|
export type { JSX } from './jsx.js';
|
|
12
12
|
export { Portal, domBinding, domMount, type DomMount, type PortalProps } from './mount.js';
|
|
13
13
|
export { pages, type PagesMessage, type PagesRequest, type PagesModel, type PagesProgram, type Pagination, } from './pages.js';
|
|
14
|
-
export { program, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
|
|
14
|
+
export { program, mapTransition, actionCommand, effectCommand, commandSlot, type CommandSlot, type Command, type Program, type Send, type Transition, } from './program.js';
|
|
15
15
|
export { available, resourceComponent, resourceError, type ResourceMessage, type ResourceModel, } from './resource.js';
|
|
16
16
|
export { fromPromise, type UiLoad, type UiPage } from './load.js';
|
|
17
17
|
export { observeBindings, inspectBindings, mountBindingInspector, type BindingInspector, type BindingInspection, observePrograms, type BindingUpdate, type ProgramUpdate, } from './diagnostics.js';
|
|
@@ -20,7 +20,10 @@ export { slot, type Slot, type CompiledContent } from './dom.js';
|
|
|
20
20
|
export { AsyncContent, type AsyncContentProps } from './AsyncContent.js';
|
|
21
21
|
export { defineTasks, type TaskDefinition, type TasksModel, type TasksMessage, type TaskResults, } from './tasks.js';
|
|
22
22
|
export { uiRuntime, type UiRuntime } from './runtime.js';
|
|
23
|
-
export { query, type Query, type QueryKey } from './query.js';
|
|
24
|
-
export { makeQueryCache, type QueryCache } from './cache.js';
|
|
23
|
+
export { query, queryGroup, type Query, type QueryKey, type QueryGroup } from './query.js';
|
|
24
|
+
export { makeQueryCache, type QueryCache, type QueryCacheOptions } from './cache.js';
|
|
25
25
|
export { queryResource, type QueryResource, observeQuery, lifetime, projectionCache, sessionGroup, type SessionContext, type Read, } from './session.js';
|
|
26
26
|
export { httpJson } from './http.js';
|
|
27
|
+
export { infiniteQuery, infiniteResource, type InfiniteData, type InfiniteQuery, type InfiniteResource, } from './infinite-query.js';
|
|
28
|
+
export { keyedTasks, type TaskHandle, type TaskOutcome } from './keyed-tasks.js';
|
|
29
|
+
export { errorBoundary } from './boundary.js';
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { mountView, view, ViewBinding } from './dom.js';
|
|
|
9
9
|
export { lazyView } from './lazy.js';
|
|
10
10
|
export { Portal, domBinding, domMount } from './mount.js';
|
|
11
11
|
export { pages, } from './pages.js';
|
|
12
|
-
export { program, actionCommand, effectCommand, commandSlot, } from './program.js';
|
|
12
|
+
export { program, mapTransition, actionCommand, effectCommand, commandSlot, } from './program.js';
|
|
13
13
|
export { available, resourceComponent, resourceError, } from './resource.js';
|
|
14
14
|
export { fromPromise } from './load.js';
|
|
15
15
|
export { observeBindings, inspectBindings, mountBindingInspector, observePrograms, } from './diagnostics.js';
|
|
@@ -18,7 +18,10 @@ export { slot } from './dom.js';
|
|
|
18
18
|
export { AsyncContent } from './AsyncContent.js';
|
|
19
19
|
export { defineTasks, } from './tasks.js';
|
|
20
20
|
export { uiRuntime } from './runtime.js';
|
|
21
|
-
export { query } from './query.js';
|
|
21
|
+
export { query, queryGroup } from './query.js';
|
|
22
22
|
export { makeQueryCache } from './cache.js';
|
|
23
23
|
export { queryResource, observeQuery, lifetime, projectionCache, sessionGroup, } from './session.js';
|
|
24
24
|
export { httpJson } from './http.js';
|
|
25
|
+
export { infiniteQuery, infiniteResource, } from './infinite-query.js';
|
|
26
|
+
export { keyedTasks } from './keyed-tasks.js';
|
|
27
|
+
export { errorBoundary } from './boundary.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { type Query, type QueryArgs, type QueryGroup } from './query.js';
|
|
3
|
+
import type { QueryCache } from './cache.js';
|
|
4
|
+
import { type QueryResource } from './session.js';
|
|
5
|
+
import { type Snapshot } from './snapshot.js';
|
|
6
|
+
export interface InfiniteData<A, Param> {
|
|
7
|
+
readonly pages: readonly {
|
|
8
|
+
readonly param: Param;
|
|
9
|
+
readonly value: A;
|
|
10
|
+
}[];
|
|
11
|
+
readonly next: Param | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface InfiniteQuery<Args, A, Param, E = never, R = never> {
|
|
14
|
+
readonly query: Query<Args, InfiniteData<A, Param>, E, R>;
|
|
15
|
+
readonly page: Query<{
|
|
16
|
+
args: Args;
|
|
17
|
+
param: Param;
|
|
18
|
+
}, A, E, R>;
|
|
19
|
+
readonly maxPages: number;
|
|
20
|
+
readonly next: (value: Snapshot<A>, param: Snapshot<Param>) => Param | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Shared pages with explicit cursor identity. Refresh preserves retained page parameters by default. */
|
|
23
|
+
export declare function infiniteQuery<Args, A, Param, E = never, R = never>(definition: {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly initial: Param;
|
|
26
|
+
readonly load: (args: Snapshot<Args>, param: Snapshot<Param>) => Effect.Effect<A, E, R>;
|
|
27
|
+
readonly next: (value: Snapshot<A>, param: Snapshot<Param>) => Param | undefined;
|
|
28
|
+
readonly maxPages?: number;
|
|
29
|
+
readonly refresh?: 'retained' | 'first';
|
|
30
|
+
readonly groups?: readonly QueryGroup[];
|
|
31
|
+
} & (Args extends QueryArgs<Args> ? unknown : {
|
|
32
|
+
readonly nonSerializableQueryArguments: never;
|
|
33
|
+
}) & (Param extends QueryArgs<Param> ? unknown : {
|
|
34
|
+
readonly nonSerializablePageParameters: never;
|
|
35
|
+
})): InfiniteQuery<Args, A, Param, E, R>;
|
|
36
|
+
export interface InfiniteResource<Args, A, Param, E = never> extends QueryResource<Args, InfiniteData<A, Param>, E> {
|
|
37
|
+
loadNext(): Effect.Effect<Snapshot<InfiniteData<A, Param>>, E>;
|
|
38
|
+
retryPage(param: Param | Snapshot<Param>): Effect.Effect<Snapshot<InfiniteData<A, Param>>, E>;
|
|
39
|
+
seed(args: Args | Snapshot<Args>, data: InfiniteData<A, Param> | Snapshot<InfiniteData<A, Param>>): Snapshot<InfiniteData<A, Param>>;
|
|
40
|
+
}
|
|
41
|
+
/** Cache-owned results, observer-owned subscriptions. Operations return typed Effects for task composition. */
|
|
42
|
+
export declare function infiniteResource<Args, A, Param, E, R>(cache: QueryCache<R>, definition: InfiniteQuery<Args, A, Param, E, NoInfer<R>>): InfiniteResource<Args, A, Param, E>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { query, encodeQueryKey, } from './query.js';
|
|
3
|
+
import { cacheInternals } from './cache-internals.js';
|
|
4
|
+
import { queryResource } from './session.js';
|
|
5
|
+
import { protectSnapshot } from './snapshot.js';
|
|
6
|
+
/** Shared pages with explicit cursor identity. Refresh preserves retained page parameters by default. */
|
|
7
|
+
export function infiniteQuery(definition) {
|
|
8
|
+
const config = Object.freeze({ ...definition });
|
|
9
|
+
const maxPages = config.maxPages ?? Infinity;
|
|
10
|
+
if (maxPages !== Infinity && (!Number.isInteger(maxPages) || maxPages < 1))
|
|
11
|
+
throw new RangeError('maxPages must be a positive integer.');
|
|
12
|
+
encodeQueryKey(config.initial);
|
|
13
|
+
const initial = protectSnapshot(config.initial);
|
|
14
|
+
const next = (value, param) => {
|
|
15
|
+
const result = config.next(value, param);
|
|
16
|
+
encodeQueryKey(result);
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
const page = query({
|
|
20
|
+
name: `${config.name}:page`,
|
|
21
|
+
load: ({ args, param }) => config.load(args, param),
|
|
22
|
+
});
|
|
23
|
+
const root = query({
|
|
24
|
+
name: config.name,
|
|
25
|
+
...(config.groups ? { groups: config.groups } : {}),
|
|
26
|
+
load: (args, previous) => {
|
|
27
|
+
const params = config.refresh !== 'first' && previous?.pages.length
|
|
28
|
+
? previous.pages.map((page) => page.param)
|
|
29
|
+
: [initial];
|
|
30
|
+
return Effect.forEach(params, (param) => config.load(args, param).pipe(Effect.map((value) => ({ param: param, value })))).pipe(Effect.map((pages) => {
|
|
31
|
+
const last = pages[pages.length - 1];
|
|
32
|
+
return {
|
|
33
|
+
pages,
|
|
34
|
+
next: next(protectSnapshot(last.value), last.param),
|
|
35
|
+
};
|
|
36
|
+
}));
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return Object.freeze({ query: root, page, maxPages, next });
|
|
40
|
+
}
|
|
41
|
+
const operations = new WeakMap();
|
|
42
|
+
function operationBook(cache, definition) {
|
|
43
|
+
let definitions = operations.get(cache);
|
|
44
|
+
if (!definitions) {
|
|
45
|
+
definitions = new WeakMap();
|
|
46
|
+
operations.set(cache, definitions);
|
|
47
|
+
}
|
|
48
|
+
let book = definitions.get(definition);
|
|
49
|
+
if (!book) {
|
|
50
|
+
book = new Map();
|
|
51
|
+
definitions.set(definition, book);
|
|
52
|
+
}
|
|
53
|
+
return book;
|
|
54
|
+
}
|
|
55
|
+
/** Cache-owned results, observer-owned subscriptions. Operations return typed Effects for task composition. */
|
|
56
|
+
export function infiniteResource(cache, definition) {
|
|
57
|
+
const resource = queryResource({ cache }, definition.query);
|
|
58
|
+
const internal = cacheInternals(cache);
|
|
59
|
+
let selected;
|
|
60
|
+
let disposed = false;
|
|
61
|
+
const stopReset = cache.onReset(() => {
|
|
62
|
+
selected = undefined;
|
|
63
|
+
});
|
|
64
|
+
const book = operationBook(cache, definition.query);
|
|
65
|
+
const updatePage = (retry) => Effect.gen(function* () {
|
|
66
|
+
if (disposed || selected === undefined)
|
|
67
|
+
return yield* Effect.interrupt;
|
|
68
|
+
const args = selected;
|
|
69
|
+
const current = yield* cache.prefetch(definition.query, args);
|
|
70
|
+
const param = retry ? retry.param : current.next;
|
|
71
|
+
if (param === undefined)
|
|
72
|
+
return current;
|
|
73
|
+
const pageKey = encodeQueryKey(param);
|
|
74
|
+
const retained = current.pages.some((page) => encodeQueryKey(page.param) === pageKey);
|
|
75
|
+
if (retry && !retained && encodeQueryKey(current.next) !== pageKey)
|
|
76
|
+
return yield* Effect.die(new RangeError('Retry a retained page or the next page parameter.'));
|
|
77
|
+
const key = encodeQueryKey(args);
|
|
78
|
+
const revision = internal.revision(definition.query, args);
|
|
79
|
+
let operation = book.get(key);
|
|
80
|
+
if (!operation || operation.revision !== revision) {
|
|
81
|
+
operation = { revision, active: 0 };
|
|
82
|
+
book.set(key, operation);
|
|
83
|
+
}
|
|
84
|
+
const state = operation;
|
|
85
|
+
state.active++;
|
|
86
|
+
return yield* Effect.gen(function* () {
|
|
87
|
+
const pageArgs = { args, param };
|
|
88
|
+
const value = yield* cache.prefetch(definition.page, pageArgs, { refresh: true });
|
|
89
|
+
let result;
|
|
90
|
+
cache.batch(() => {
|
|
91
|
+
const latest = cache.getQueryData(definition.query, args);
|
|
92
|
+
result = latest;
|
|
93
|
+
// Other page operations can merge. External refresh, writes and reset own a new revision.
|
|
94
|
+
if (!latest ||
|
|
95
|
+
book.get(key) !== state ||
|
|
96
|
+
state.revision !== internal.revision(definition.query, args))
|
|
97
|
+
return;
|
|
98
|
+
const index = latest.pages.findIndex((page) => encodeQueryKey(page.param) === pageKey);
|
|
99
|
+
if (index < 0 && (retained || encodeQueryKey(latest.next) !== pageKey))
|
|
100
|
+
return;
|
|
101
|
+
if (index >= 0 && latest.pages[index].value === value)
|
|
102
|
+
return;
|
|
103
|
+
const pages = [...latest.pages];
|
|
104
|
+
const page = { param, value };
|
|
105
|
+
if (index >= 0)
|
|
106
|
+
pages[index] = page;
|
|
107
|
+
else
|
|
108
|
+
pages.push(page);
|
|
109
|
+
const kept = pages.slice(-definition.maxPages);
|
|
110
|
+
const last = kept[kept.length - 1];
|
|
111
|
+
result = cache.setQueryData(definition.query, args, {
|
|
112
|
+
pages: kept,
|
|
113
|
+
next: definition.next(last.value, last.param),
|
|
114
|
+
});
|
|
115
|
+
state.revision = internal.revision(definition.query, args);
|
|
116
|
+
});
|
|
117
|
+
return result ?? (yield* Effect.interrupt);
|
|
118
|
+
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
119
|
+
state.active--;
|
|
120
|
+
if (state.active === 0 && book.get(key) === state)
|
|
121
|
+
book.delete(key);
|
|
122
|
+
})));
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
read: resource.read,
|
|
126
|
+
subscribe: resource.subscribe,
|
|
127
|
+
select(args) {
|
|
128
|
+
selected = args;
|
|
129
|
+
resource.select(args);
|
|
130
|
+
},
|
|
131
|
+
refresh: resource.refresh,
|
|
132
|
+
loadNext: () => updatePage(),
|
|
133
|
+
retryPage: (param) => updatePage({ param }),
|
|
134
|
+
seed(args, data) {
|
|
135
|
+
if (!data.pages.length || data.pages.length > definition.maxPages)
|
|
136
|
+
throw new RangeError('Seed pages must fit the retained page range.');
|
|
137
|
+
encodeQueryKey(data.next);
|
|
138
|
+
const keys = data.pages.map((page) => encodeQueryKey(page.param));
|
|
139
|
+
if (new Set(keys).size !== keys.length)
|
|
140
|
+
throw new TypeError('Seed page parameters must be unique.');
|
|
141
|
+
return cache.setQueryData(definition.query, args, data);
|
|
142
|
+
},
|
|
143
|
+
dispose() {
|
|
144
|
+
disposed = true;
|
|
145
|
+
selected = undefined;
|
|
146
|
+
stopReset();
|
|
147
|
+
resource.dispose();
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { compiledSlots } from './slotIdentity.js';
|
|
2
|
+
// Compiler implementation detail. An untyped receiver's method name is not
|
|
3
|
+
// evidence that it is a standard data operation. Validate its descriptor before
|
|
4
|
+
// invoking it, without running a user getter in the process.
|
|
5
|
+
const prototypes = [
|
|
6
|
+
Array.prototype,
|
|
7
|
+
String.prototype,
|
|
8
|
+
Number.prototype,
|
|
9
|
+
Boolean.prototype,
|
|
10
|
+
BigInt.prototype,
|
|
11
|
+
Function.prototype,
|
|
12
|
+
Date.prototype,
|
|
13
|
+
RegExp.prototype,
|
|
14
|
+
Map.prototype,
|
|
15
|
+
Set.prototype,
|
|
16
|
+
URL.prototype,
|
|
17
|
+
URLSearchParams.prototype,
|
|
18
|
+
Blob.prototype,
|
|
19
|
+
File.prototype,
|
|
20
|
+
Object.getPrototypeOf(Uint8Array.prototype),
|
|
21
|
+
Intl.DateTimeFormat.prototype,
|
|
22
|
+
Intl.NumberFormat.prototype,
|
|
23
|
+
Intl.Collator.prototype,
|
|
24
|
+
Intl.RelativeTimeFormat.prototype,
|
|
25
|
+
Intl.PluralRules.prototype,
|
|
26
|
+
Intl.ListFormat.prototype,
|
|
27
|
+
Intl.Segmenter.prototype,
|
|
28
|
+
];
|
|
29
|
+
const operations = new Map();
|
|
30
|
+
for (const prototype of prototypes) {
|
|
31
|
+
for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(prototype))) {
|
|
32
|
+
if (name === 'constructor')
|
|
33
|
+
continue;
|
|
34
|
+
const entries = operations.get(name) ?? [];
|
|
35
|
+
entries.push(descriptor);
|
|
36
|
+
operations.set(name, entries);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Preserve receiver identity and JavaScript call/optional-chain semantics. */
|
|
40
|
+
export function intrinsic(receiver, path, method) {
|
|
41
|
+
checkPath(receiver, path, 0, method);
|
|
42
|
+
return receiver;
|
|
43
|
+
}
|
|
44
|
+
function checkPath(target, path, index, method) {
|
|
45
|
+
if (target === null || target === undefined)
|
|
46
|
+
return;
|
|
47
|
+
if (index === path.length) {
|
|
48
|
+
checkIntrinsic(target, method);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const field = path[index];
|
|
52
|
+
if (field === null) {
|
|
53
|
+
if (Array.isArray(target)) {
|
|
54
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(target))) {
|
|
55
|
+
if (!/^(0|[1-9]\d*)$/u.test(key))
|
|
56
|
+
continue;
|
|
57
|
+
if (!Object.hasOwn(descriptor, 'value'))
|
|
58
|
+
throw new Error('EffectWeb render data cannot use an array accessor. Pass immutable values.');
|
|
59
|
+
checkPath(descriptor.value, path, index + 1, method);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (target instanceof Map) {
|
|
63
|
+
for (const value of Map.prototype.values.call(target))
|
|
64
|
+
checkPath(value, path, index + 1, method);
|
|
65
|
+
}
|
|
66
|
+
else if (target instanceof Set) {
|
|
67
|
+
for (const value of Set.prototype.values.call(target))
|
|
68
|
+
checkPath(value, path, index + 1, method);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let object = Object(target);
|
|
73
|
+
while (object) {
|
|
74
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, field);
|
|
75
|
+
if (descriptor) {
|
|
76
|
+
if (Object.hasOwn(descriptor, 'value')) {
|
|
77
|
+
checkPath(descriptor.value, path, index + 1, method);
|
|
78
|
+
}
|
|
79
|
+
else if (descriptor.get &&
|
|
80
|
+
operations.get(field)?.some((operation) => operation.get === descriptor.get)) {
|
|
81
|
+
// Native getters are checked by identity before execution. File/Blob
|
|
82
|
+
// metadata and collection sizes expose data through readonly accessors.
|
|
83
|
+
checkPath(descriptor.get.call(target), path, index + 1, method);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
throw new Error('EffectWeb render data cannot use an accessor to supply a standard data operation. Pass immutable data or a checked helper.');
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
object = Object.getPrototypeOf(object);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function checkIntrinsic(receiver, method) {
|
|
94
|
+
if (receiver === null || receiver === undefined)
|
|
95
|
+
return;
|
|
96
|
+
if (method === '@slot') {
|
|
97
|
+
if (typeof receiver !== 'function' || !compiledSlots.has(receiver))
|
|
98
|
+
throw new Error('EffectWeb render callbacks must be compiled slots. Declare markup with slot(), or use a checked helper with an explicit purity contract.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let object = Object(receiver);
|
|
102
|
+
while (object) {
|
|
103
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, method);
|
|
104
|
+
if (descriptor) {
|
|
105
|
+
const known = operations
|
|
106
|
+
.get(method)
|
|
107
|
+
?.some((operation) => Object.hasOwn(descriptor, 'value')
|
|
108
|
+
? typeof descriptor.value === 'function' && descriptor.value === operation.value
|
|
109
|
+
: descriptor.get !== undefined &&
|
|
110
|
+
descriptor.get === operation.get &&
|
|
111
|
+
descriptor.set === undefined);
|
|
112
|
+
if (!known)
|
|
113
|
+
throw new Error(`EffectWeb cannot prove .${method} is a standard data operation. Use a checked local helper or an imported helper with a purity contract.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
object = Object.getPrototypeOf(object);
|
|
117
|
+
}
|
|
118
|
+
// The original member call retains its own missing/optional-method behavior.
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Cause, Effect } from 'effect';
|
|
2
|
+
import { type TaskPolicy } from './program.js';
|
|
3
|
+
import type { DisposableOwner } from './owner.js';
|
|
4
|
+
import { type UiRuntime } from './runtime.js';
|
|
5
|
+
import { type Snapshot } from './snapshot.js';
|
|
6
|
+
export type TaskOutcome<A, E> = {
|
|
7
|
+
readonly _tag: 'Success';
|
|
8
|
+
readonly value: Snapshot<A>;
|
|
9
|
+
} | {
|
|
10
|
+
readonly _tag: 'Failure';
|
|
11
|
+
readonly cause: Cause.Cause<E>;
|
|
12
|
+
} | {
|
|
13
|
+
readonly _tag: 'Cancelled' | 'Superseded' | 'Dropped';
|
|
14
|
+
};
|
|
15
|
+
export interface TaskHandle<A, E> {
|
|
16
|
+
readonly outcome: Promise<TaskOutcome<A, E>>;
|
|
17
|
+
}
|
|
18
|
+
/** Per-entity serialization. Inputs are captured now; run reads services when work starts. */
|
|
19
|
+
export declare function keyedTasks<Key, Input, A, E = never, R = never>(owner: DisposableOwner, definition: {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly policy: TaskPolicy;
|
|
22
|
+
readonly run: (key: Key, input: Snapshot<Input>) => Effect.Effect<A, E, R>;
|
|
23
|
+
}, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): {
|
|
24
|
+
submit(key: Key, input: Input | Snapshot<Input>): TaskHandle<A, E>;
|
|
25
|
+
cancel(key: Key): void;
|
|
26
|
+
drain(...selected: [] | [Key]): Promise<void>;
|
|
27
|
+
close: () => Promise<void>;
|
|
28
|
+
dispose: () => void;
|
|
29
|
+
};
|