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/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, owner.report);
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
- return Object.assign((value) => new SlotPlacement(definition, value), {
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
- return () => {
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
- scope.jobs.push(() => active?.set(scope.value));
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';
@@ -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
  };
@@ -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,2 @@
1
+ /** Preserve receiver identity and JavaScript call/optional-chain semantics. */
2
+ export declare function intrinsic<T>(receiver: T, path: readonly (string | null)[], method: string): T;
@@ -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
+ };