effectweb 0.2.3 → 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 (62) hide show
  1. package/README.md +93 -1
  2. package/dist/AsyncContent.d.ts +2 -2
  3. package/dist/AsyncContent.js +6 -3
  4. package/dist/actions.d.ts +4 -3
  5. package/dist/actions.js +1 -0
  6. package/dist/cache-internals.d.ts +14 -0
  7. package/dist/cache-internals.js +11 -0
  8. package/dist/cache.d.ts +17 -14
  9. package/dist/cache.js +98 -26
  10. package/dist/collection.d.ts +10 -9
  11. package/dist/collection.js +32 -6
  12. package/dist/component.d.ts +8 -7
  13. package/dist/diagnostics.d.ts +25 -0
  14. package/dist/diagnostics.js +167 -1
  15. package/dist/dom-attributes.json +71 -0
  16. package/dist/dom.d.ts +33 -12
  17. package/dist/dom.js +466 -80
  18. package/dist/form.d.ts +61 -0
  19. package/dist/form.js +98 -0
  20. package/dist/html-attributes.d.ts +602 -0
  21. package/dist/html-attributes.js +1 -0
  22. package/dist/http.d.ts +5 -0
  23. package/dist/http.js +7 -0
  24. package/dist/index.d.ts +12 -10
  25. package/dist/index.js +7 -5
  26. package/dist/jsx.d.ts +165 -33
  27. package/dist/jsx.js +2 -1
  28. package/dist/lazy.d.ts +23 -0
  29. package/dist/lazy.js +58 -0
  30. package/dist/load.d.ts +3 -3
  31. package/dist/mount.d.ts +8 -11
  32. package/dist/mount.js +1 -3
  33. package/dist/owner.d.ts +11 -9
  34. package/dist/owner.js +27 -36
  35. package/dist/pages.d.ts +23 -22
  36. package/dist/pages.js +15 -9
  37. package/dist/program.d.ts +19 -9
  38. package/dist/program.js +145 -49
  39. package/dist/query-internals.d.ts +11 -0
  40. package/dist/query-internals.js +10 -0
  41. package/dist/query.d.ts +24 -19
  42. package/dist/query.js +60 -7
  43. package/dist/resource.d.ts +4 -10
  44. package/dist/resource.js +14 -5
  45. package/dist/runtime.d.ts +1 -2
  46. package/dist/runtime.js +3 -3
  47. package/dist/session.d.ts +12 -15
  48. package/dist/session.js +15 -12
  49. package/dist/share.d.ts +5 -4
  50. package/dist/share.js +5 -44
  51. package/dist/sharing.d.ts +6 -0
  52. package/dist/sharing.js +93 -0
  53. package/dist/snapshot.d.ts +16 -5
  54. package/dist/snapshot.js +6 -7
  55. package/dist/svg-attributes.d.ts +12 -0
  56. package/dist/svg-attributes.js +1 -0
  57. package/dist/task.d.ts +9 -8
  58. package/dist/task.js +75 -47
  59. package/dist/tasks.d.ts +20 -15
  60. package/dist/tasks.js +39 -25
  61. package/dist/testing.d.ts +10 -14
  62. package/package.json +5 -1
@@ -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,16 +44,22 @@ 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;
@@ -54,14 +67,22 @@ export declare function element(parent: Node, before: Node | null, tag: string):
54
67
  export declare function attribute(element: Element, name: string, value: unknown): void;
55
68
  /** Compiler output for attributes whose unchanged value needs no DOM refresh. */
56
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;
57
72
  export declare function text<M, E>(scope: Scope<M, E>, parent: Node, before: Node | null, dependencies: Dependencies, read: () => unknown, source?: BindingSource): void;
58
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;
59
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;
60
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;
61
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;
62
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;
63
- export declare function attach<M, E>(scope: Scope<M, E>, element: Element, dependencies: Dependencies, read: () => DomMount | undefined): void;
64
- 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;
65
86
  /** Build static markup once per document/namespace, then clone native nodes at each use. */
66
87
  export declare function template(build: string | ((parent: Node, before: Node | null) => void), depth?: number): (parent: Node, before: Node | null) => Node;
67
88
  export declare function literal(parent: Node, before: Node | null, value: string): void;