effectweb 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +94 -2
  3. package/dist/AsyncContent.d.ts +2 -2
  4. package/dist/AsyncContent.js +36 -24
  5. package/dist/actions.d.ts +4 -4
  6. package/dist/actions.js +1 -1
  7. package/dist/cache-internals.d.ts +14 -0
  8. package/dist/cache-internals.js +11 -0
  9. package/dist/cache.d.ts +17 -15
  10. package/dist/cache.js +98 -27
  11. package/dist/collection.d.ts +10 -10
  12. package/dist/collection.js +32 -6
  13. package/dist/component.d.ts +8 -7
  14. package/dist/diagnostics.d.ts +25 -0
  15. package/dist/diagnostics.js +167 -1
  16. package/dist/dom-attributes.json +71 -0
  17. package/dist/dom.d.ts +36 -13
  18. package/dist/dom.js +547 -84
  19. package/dist/form.d.ts +61 -0
  20. package/dist/form.js +98 -0
  21. package/dist/html-attributes.d.ts +602 -0
  22. package/dist/html-attributes.js +1 -0
  23. package/dist/http.d.ts +5 -0
  24. package/dist/http.js +7 -0
  25. package/dist/index.d.ts +12 -10
  26. package/dist/index.js +7 -5
  27. package/dist/jsx.d.ts +165 -33
  28. package/dist/jsx.js +2 -1
  29. package/dist/lazy.d.ts +23 -0
  30. package/dist/lazy.js +58 -0
  31. package/dist/load.d.ts +3 -4
  32. package/dist/mount.d.ts +8 -11
  33. package/dist/mount.js +1 -3
  34. package/dist/owner.d.ts +11 -9
  35. package/dist/owner.js +27 -38
  36. package/dist/pages.d.ts +23 -23
  37. package/dist/pages.js +15 -10
  38. package/dist/program.d.ts +19 -10
  39. package/dist/program.js +172 -66
  40. package/dist/query-internals.d.ts +11 -0
  41. package/dist/query-internals.js +10 -0
  42. package/dist/query.d.ts +24 -20
  43. package/dist/query.js +60 -7
  44. package/dist/resource.d.ts +4 -10
  45. package/dist/resource.js +14 -5
  46. package/dist/runtime.d.ts +1 -2
  47. package/dist/runtime.js +3 -3
  48. package/dist/session.d.ts +12 -18
  49. package/dist/session.js +15 -15
  50. package/dist/share.d.ts +5 -4
  51. package/dist/share.js +5 -44
  52. package/dist/sharing.d.ts +6 -0
  53. package/dist/sharing.js +93 -0
  54. package/dist/snapshot.d.ts +16 -5
  55. package/dist/snapshot.js +6 -7
  56. package/dist/state.d.ts +0 -1
  57. package/dist/state.js +0 -1
  58. package/dist/svg-attributes.d.ts +12 -0
  59. package/dist/svg-attributes.js +1 -0
  60. package/dist/task.d.ts +9 -8
  61. package/dist/task.js +75 -47
  62. package/dist/tasks.d.ts +20 -15
  63. package/dist/tasks.js +39 -25
  64. package/dist/testing.d.ts +10 -15
  65. package/package.json +5 -1
@@ -1,14 +1,15 @@
1
+ import type { Snapshot } from './snapshot.js';
1
2
  import type { View } from './dom.js';
2
3
  import type { Transition } from './program.js';
3
4
  import { type UiRuntime } from './runtime.js';
4
5
  /** Local fields with no command lifecycle. Sends are shallow patches, never updater callbacks. */
5
6
  export declare function localComponent<Props, State extends object>(definition: {
6
- init: (props: Props) => State & {
7
+ init: (props: Snapshot<Props>) => (State | Snapshot<State>) & {
7
8
  readonly props?: never;
8
9
  };
9
10
  view: View<State & {
10
11
  readonly props: Props;
11
- }, Partial<State> & {
12
+ }, (Partial<State> | Partial<Snapshot<State>>) & {
12
13
  readonly props?: never;
13
14
  }>;
14
15
  }): View<Props, never>;
@@ -16,9 +17,9 @@ export declare function localComponent<Props, State extends object>(definition:
16
17
  export declare function component<Props, Model extends {
17
18
  readonly props: Props;
18
19
  }, Message, R = never>(definition: {
19
- init: (props: Props) => Model;
20
- receive?: (model: Model, props: Props) => Transition<Model, Message, R>;
21
- update: (model: Model, message: Message) => Transition<Model, Message, R>;
20
+ init: (props: Snapshot<Props>) => Model | Snapshot<Model>;
21
+ receive?: (model: Snapshot<Model>, props: Snapshot<Props>) => Transition<Model, Message, R>;
22
+ update: (model: Snapshot<Model>, message: Message) => Transition<Model, Message, R>;
22
23
  view: View<Model, Message>;
23
24
  } & ([R] extends [never] ? {
24
25
  runtime?: UiRuntime<R>;
@@ -27,7 +28,7 @@ export declare function component<Props, Model extends {
27
28
  })): View<Props, never>;
28
29
  /** Mount an existing program without introducing a second state owner. */
29
30
  export declare function programView<Props, Model, Message>(definition: {
30
- create: (props: Props) => import('./program').Program<Model, Message>;
31
- receive: (source: import('./program').Program<Model, Message>, props: Props) => void;
31
+ create: (props: Snapshot<Props>) => import('./program').Program<Model, Message>;
32
+ receive: (source: import('./program').Program<Model, Message>, props: Snapshot<Props>) => void;
32
33
  view: View<Model, Message>;
33
34
  }): View<Props, never>;
@@ -10,6 +10,7 @@ export interface BindingUpdate {
10
10
  readonly source: BindingSource;
11
11
  readonly changed: readonly string[];
12
12
  readonly kind: 'derive' | 'binding';
13
+ readonly reason: 'initial' | 'dependencies';
13
14
  }
14
15
  /** Each registration has its own lifetime; observers can be stopped in any order. */
15
16
  export declare function observeBindings(next: (update: BindingUpdate) => void): () => void;
@@ -30,3 +31,27 @@ export declare function observePrograms(limit?: number): {
30
31
  events: () => ProgramUpdate[];
31
32
  dispose: () => boolean;
32
33
  };
34
+ /** Counts aggregate all mounted instances of a source expression. */
35
+ export interface BindingInspection {
36
+ readonly source: BindingSource;
37
+ readonly derives: number;
38
+ readonly bindings: number;
39
+ readonly changed: readonly string[];
40
+ readonly reason: BindingUpdate['reason'];
41
+ }
42
+ export interface BindingInspector {
43
+ /** Most recently updated first; bounded by limit (default 200, maximum 1000). */
44
+ entries(): readonly BindingInspection[];
45
+ subscribe(next: () => void): () => void;
46
+ clear(): void;
47
+ /** Unsubscribe and release all metadata and listeners. */
48
+ dispose(): void;
49
+ }
50
+ /** Development metadata only: never reads or retains models, values, or DOM nodes. */
51
+ export declare function inspectBindings(options?: {
52
+ readonly limit?: number;
53
+ }): BindingInspector;
54
+ /** Mount a live source inspector. Mount before the application to include initial evaluations.
55
+ * The returned cleanup removes the panel; an externally supplied inspector keeps its lifetime.
56
+ */
57
+ export declare function mountBindingInspector(target: HTMLElement, inspector?: BindingInspector): () => void;
@@ -14,10 +14,11 @@ export function traceBinding(source, previous, next, kind) {
14
14
  const update = {
15
15
  source,
16
16
  kind,
17
+ reason: previous ? 'dependencies' : 'initial',
17
18
  changed: source.dependencies.filter((_, index) => !previous || !Object.is(previous[index], next[index])),
18
19
  };
19
20
  // Observers registered during dispatch start with the next update.
20
- // oxlint-disable-next-line unicorn/no-useless-spread
21
+ // oxlint-disable-next-line unicorn/no-useless-spread -- Iterate a snapshot because listeners can add or remove subscriptions.
21
22
  for (const [token, observer] of [...observers]) {
22
23
  if (!observers.has(token))
23
24
  continue;
@@ -55,3 +56,168 @@ export function observePrograms(limit = 100) {
55
56
  programObservers.add(receive);
56
57
  return { events: () => entries.slice(), dispose: () => programObservers.delete(receive) };
57
58
  }
59
+ /** Development metadata only: never reads or retains models, values, or DOM nodes. */
60
+ export function inspectBindings(options = {}) {
61
+ const requested = options.limit ?? 200;
62
+ const limit = Number.isFinite(requested)
63
+ ? Math.max(1, Math.min(1000, Math.floor(requested)))
64
+ : 200;
65
+ const records = new Map();
66
+ const listeners = new Set();
67
+ let disposed = false;
68
+ const notify = () => {
69
+ // New subscribers start with the next event.
70
+ // oxlint-disable-next-line unicorn/no-useless-spread -- Iterate a snapshot because listeners can add or remove subscriptions.
71
+ for (const listener of [...listeners]) {
72
+ if (!listeners.has(listener))
73
+ continue;
74
+ try {
75
+ listener();
76
+ }
77
+ catch (error) {
78
+ reportSafely(reportError, error);
79
+ }
80
+ }
81
+ };
82
+ const stop = observeBindings((update) => {
83
+ const source = update.source;
84
+ const key = JSON.stringify([source.file, source.line, source.column, source.expression]);
85
+ const previous = records.get(key);
86
+ const record = Object.freeze({
87
+ source: Object.freeze({
88
+ file: source.file,
89
+ line: source.line,
90
+ column: source.column,
91
+ expression: source.expression,
92
+ dependencies: Object.freeze([...source.dependencies]),
93
+ }),
94
+ derives: (previous?.derives ?? 0) + Number(update.kind === 'derive'),
95
+ bindings: (previous?.bindings ?? 0) + Number(update.kind === 'binding'),
96
+ changed: Object.freeze([...update.changed]),
97
+ reason: update.reason,
98
+ });
99
+ records.delete(key);
100
+ records.set(key, record);
101
+ if (records.size > limit)
102
+ records.delete(records.keys().next().value);
103
+ notify();
104
+ });
105
+ return {
106
+ entries: () => [...records.values()].reverse(),
107
+ subscribe(next) {
108
+ if (!disposed)
109
+ listeners.add(next);
110
+ return () => {
111
+ listeners.delete(next);
112
+ };
113
+ },
114
+ clear() {
115
+ records.clear();
116
+ if (!disposed)
117
+ notify();
118
+ },
119
+ dispose() {
120
+ disposed = true;
121
+ stop();
122
+ records.clear();
123
+ listeners.clear();
124
+ },
125
+ };
126
+ }
127
+ /** Mount a live source inspector. Mount before the application to include initial evaluations.
128
+ * The returned cleanup removes the panel; an externally supplied inspector keeps its lifetime.
129
+ */
130
+ export function mountBindingInspector(target, inspector) {
131
+ const inspection = inspector ?? inspectBindings();
132
+ const document = target.ownerDocument;
133
+ const panel = document.createElement('section');
134
+ panel.setAttribute('aria-label', 'EffectWeb dependency inspector');
135
+ const heading = document.createElement('h2');
136
+ heading.textContent = 'EffectWeb dependencies';
137
+ const hint = document.createElement('p');
138
+ hint.textContent =
139
+ 'Development builds only. Counts include initial evaluations and aggregate mounted instances. Old sources are evicted.';
140
+ const filter = document.createElement('input');
141
+ filter.type = 'search';
142
+ filter.placeholder = 'Filter file, expression or dependency';
143
+ filter.setAttribute('aria-label', filter.placeholder);
144
+ const clear = document.createElement('button');
145
+ clear.type = 'button';
146
+ clear.textContent = 'Clear counts';
147
+ const table = document.createElement('table');
148
+ const header = table.createTHead().insertRow();
149
+ for (const label of [
150
+ 'Source expression',
151
+ 'Inferred dependencies',
152
+ 'Latest reason',
153
+ 'Derives',
154
+ 'Bindings',
155
+ ]) {
156
+ const cell = document.createElement('th');
157
+ cell.scope = 'col';
158
+ cell.textContent = label;
159
+ header.append(cell);
160
+ }
161
+ const body = table.createTBody();
162
+ const status = document.createElement('p');
163
+ let active = true;
164
+ let queued = false;
165
+ const render = () => {
166
+ queued = false;
167
+ if (!active)
168
+ return;
169
+ const search = filter.value.toLowerCase();
170
+ const entries = inspection
171
+ .entries()
172
+ .filter((entry) => [entry.source.file, entry.source.expression, ...entry.source.dependencies]
173
+ .join(' ')
174
+ .toLowerCase()
175
+ .includes(search));
176
+ body.replaceChildren();
177
+ for (const entry of entries) {
178
+ const row = body.insertRow();
179
+ const source = entry.source;
180
+ const values = [
181
+ `${source.file}:${source.line}:${source.column}\n${source.expression}`,
182
+ source.dependencies.join(', ') || 'No snapshot dependencies',
183
+ entry.reason === 'initial'
184
+ ? 'Initial evaluation'
185
+ : `Changed: ${entry.changed.join(', ') || 'dependency value'}`,
186
+ String(entry.derives),
187
+ String(entry.bindings),
188
+ ];
189
+ for (const value of values) {
190
+ const cell = row.insertCell();
191
+ cell.textContent = value;
192
+ cell.style.whiteSpace = 'pre-wrap';
193
+ cell.style.verticalAlign = 'top';
194
+ cell.style.padding = '0.4em';
195
+ }
196
+ }
197
+ status.textContent = entries.length
198
+ ? `${entries.length} source expressions`
199
+ : 'No matching evaluations. Interact with an application compiled in development mode.';
200
+ };
201
+ const schedule = () => {
202
+ if (!queued && active) {
203
+ queued = true;
204
+ queueMicrotask(render);
205
+ }
206
+ };
207
+ const reset = () => inspection.clear();
208
+ filter.addEventListener('input', render);
209
+ clear.addEventListener('click', reset);
210
+ const stop = inspection.subscribe(schedule);
211
+ panel.append(heading, hint, filter, clear, table, status);
212
+ target.append(panel);
213
+ render();
214
+ return () => {
215
+ active = false;
216
+ stop();
217
+ filter.removeEventListener('input', render);
218
+ clear.removeEventListener('click', reset);
219
+ panel.remove();
220
+ if (!inspector)
221
+ inspection.dispose();
222
+ };
223
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "boolean": [
3
+ "allowfullscreen",
4
+ "async",
5
+ "autofocus",
6
+ "autoplay",
7
+ "capture",
8
+ "checked",
9
+ "controls",
10
+ "default",
11
+ "defer",
12
+ "disabled",
13
+ "disablepictureinpicture",
14
+ "disableremoteplayback",
15
+ "download",
16
+ "formnovalidate",
17
+ "hidden",
18
+ "inert",
19
+ "ismap",
20
+ "itemscope",
21
+ "loop",
22
+ "multiple",
23
+ "muted",
24
+ "nomodule",
25
+ "novalidate",
26
+ "open",
27
+ "playsinline",
28
+ "readonly",
29
+ "required",
30
+ "reversed",
31
+ "selected"
32
+ ],
33
+ "booleanish": ["contenteditable", "draggable", "spellcheck"],
34
+ "aliases": {
35
+ "className": "class",
36
+ "tabIndex": "tabindex",
37
+ "htmlFor": "for",
38
+ "contentEditable": "contenteditable",
39
+ "readOnly": "readonly",
40
+ "playsInline": "playsinline",
41
+ "autoFocus": "autofocus",
42
+ "autoPlay": "autoplay",
43
+ "allowFullscreen": "allowfullscreen",
44
+ "formNoValidate": "formnovalidate",
45
+ "isMap": "ismap",
46
+ "itemScope": "itemscope",
47
+ "noModule": "nomodule",
48
+ "noValidate": "novalidate",
49
+ "disablePictureInPicture": "disablepictureinpicture",
50
+ "disableRemotePlayback": "disableremoteplayback",
51
+ "acceptCharset": "accept-charset",
52
+ "httpEquiv": "http-equiv",
53
+ "classname": "class",
54
+ "htmlfor": "for",
55
+ "acceptcharset": "accept-charset",
56
+ "httpequiv": "http-equiv"
57
+ },
58
+ "properties": {
59
+ "readonly": "readOnly",
60
+ "playsinline": "playsInline",
61
+ "allowfullscreen": "allowFullscreen",
62
+ "formnovalidate": "formNoValidate",
63
+ "ismap": "isMap",
64
+ "itemscope": "itemScope",
65
+ "nomodule": "noModule",
66
+ "novalidate": "noValidate",
67
+ "disablepictureinpicture": "disablePictureInPicture",
68
+ "disableremoteplayback": "disableRemotePlayback"
69
+ },
70
+ "booleanValues": { "translate": { "true": "yes", "false": "no" } }
71
+ }
package/dist/dom.d.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import type { Snapshot } from './snapshot.js';
1
2
  import { type ReportError } from './errors.js';
2
3
  import { type BindingSource } from './diagnostics.js';
3
4
  import type { Rows } from './index.js';
4
- import type { JSX } from './jsx.js';
5
+ import { jsxComponent, type JSX } from './jsx.js';
5
6
  import type { DomMount } from './mount.js';
6
7
  import type { Program, Send } from './program.js';
7
8
  type Dependencies = () => readonly unknown[];
@@ -22,14 +23,20 @@ export declare class Scope<M, E> {
22
23
  set(value: M): void;
23
24
  dispose(): void;
24
25
  }
25
- export interface View<M, E> {
26
- (props: M): JSX.Element;
27
- (props: {
28
- model: M;
29
- send: Send<E>;
30
- }): JSX.Element;
26
+ export interface View<M, E> extends JSX.ComponentType {
27
+ (this: never, props: [E] extends [never] ? M | Snapshot<M> : never): JSX.Element;
31
28
  readonly build: Build<M, E>;
32
29
  }
30
+ /** Compiler marker for mounting a view with an explicit model and message dispatcher. */
31
+ export declare const ViewBinding: (<M, E>(this: never, _props: {
32
+ view: View<M, E>;
33
+ model: NoInfer<M> | Snapshot<NoInfer<M>>;
34
+ send: Send<NoInfer<E>>;
35
+ }) => JSX.Element) & {
36
+ [jsxComponent]: true;
37
+ };
38
+ /** Untyped callers get an actionable failure instead of silently losing child messages. */
39
+ export declare function unboundSend(_message: never): never;
33
40
  declare const contentBrand: unique symbol;
34
41
  /** Owned markup accepted wherever JSX can render content. */
35
42
  export interface CompiledContent {
@@ -37,30 +44,46 @@ export interface CompiledContent {
37
44
  }
38
45
  /** A compiled template with a typed placement value, captured in its declaring view. */
39
46
  export interface Slot<A = void> {
40
- (value: A): JSX.Element;
47
+ (value: A | Snapshot<A>): JSX.Element;
41
48
  readonly [contentBrand]: [A] extends [void] ? true : false;
42
49
  }
43
50
  /** Compiler marker: declare inside view(), or directly in a compiled component prop. */
44
- export declare function slot<A = void>(_render: (value: A) => JSX.Element): Slot<A>;
51
+ export declare function slot<A = void>(_render: (value: Snapshot<A>) => JSX.Element): Slot<A>;
45
52
  /** Compiler output. Captures follow their declaring scope; placements own independent scopes. */
46
53
  export declare function compiledSlot<M, E, A>(owner: Scope<M, E>, build: Build<A, E>): Slot<A>;
47
54
  /** This declaration is a compiler marker, never a component setup callback. */
48
- export declare function view<M, E = never>(_render: (model: M, send: Send<E>) => JSX.Element): View<M, E>;
55
+ export declare function view<M, E = never>(_render: (model: Snapshot<M>, send: Send<E>) => JSX.Element): View<M, E>;
49
56
  export declare function compiled<M, E>(build: Build<M, E>): View<M, E>;
57
+ export interface PortalProps {
58
+ readonly children?: JSX.Element;
59
+ readonly mount?: Element | undefined;
60
+ }
61
+ /** Owned content in an element target, defaulting to the document body. */
62
+ export declare const Portal: View<PortalProps, never>;
50
63
  export declare function mountView<M, E>(parent: Node, definition: View<M, E>, source: Program<M, E>, options?: {
51
64
  onError?: ReportError;
52
65
  }): () => void;
53
66
  export declare function element(parent: Node, before: Node | null, tag: string): Element;
54
67
  export declare function attribute(element: Element, name: string, value: unknown): void;
68
+ /** Compiler output for attributes whose unchanged value needs no DOM refresh. */
69
+ export declare function bindAttribute<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
70
+ /** Handled edits can change a control even when dispatch publishes no new model. */
71
+ export declare function bindControl<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
55
72
  export declare function text<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
56
73
  export declare function event<M, E>(scope: Scope<M, E>, element: Element, name: string, handler: (event: Event) => unknown): void;
74
+ /** One handler identity owns its requests, regardless of the JSX attribute spelling. */
75
+ export declare function bindEvent<M, E>(scope: Scope<M, E>, element: Element, name: string, dependencies: Dependencies, read: () => unknown): void;
76
+ /** Reconcile merged JSX attributes. Each host, event and control owns its cleanup. */
77
+ export declare function bindAttributes<M, E>(scope: Scope<M, E>, element: Element, dependencies: Dependencies, read: () => Readonly<Record<string, unknown>>): void;
57
78
  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;
58
79
  export declare function each<M, E, A>(scope: Scope<M, E>, parent: Node, before: Node | null, read: () => Rows<A> | readonly A[], outer: Dependencies, indexUsed: boolean, build: Build<readonly [A, number], E>): void;
59
80
  export declare function invoke<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => readonly unknown[], build: Build<readonly unknown[], E>): void;
60
81
  export declare function child<M, E, C, F>(scope: Scope<M, E>, parent: Node, before: Node | null, definition: View<C, F>, dependencies: Dependencies, model: () => C, send: Send<F>): void;
61
- export declare function attach<M, E>(scope: Scope<M, E>, element: Element, dependencies: Dependencies, read: () => DomMount | undefined): void;
62
- export declare function portal<M, E>(scope: Scope<M, E>, build: Build<M, E>): void;
82
+ /** A late-bound compiled definition owns one replaceable DOM region. */
83
+ export declare function viewRegion<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null): (next: View<M, E> | undefined) => void;
84
+ export declare function attach<M, E, T extends Element>(scope: Scope<M, E>, element: T, dependencies: Dependencies, read: () => DomMount<NoInfer<T>> | undefined): void;
85
+ export declare function portal<M, E>(scope: Scope<M, E>, build: Build<M, E>, mount?: () => Element | undefined): void;
63
86
  /** 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) => void;
87
+ export declare function template(build: string | ((parent: Node, before: Node | null) => void), depth?: number): (parent: Node, before: Node | null) => Node;
65
88
  export declare function literal(parent: Node, before: Node | null, value: string): void;
66
89
  export {};