effectweb 0.1.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/AsyncContent.d.ts +20 -0
  4. package/dist/AsyncContent.js +75 -0
  5. package/dist/actions.d.ts +27 -0
  6. package/dist/actions.js +31 -0
  7. package/dist/cache.d.ts +32 -0
  8. package/dist/cache.js +147 -0
  9. package/dist/collection.d.ts +15 -0
  10. package/dist/collection.js +24 -0
  11. package/dist/component.d.ts +33 -0
  12. package/dist/component.js +58 -0
  13. package/dist/diagnostics.d.ts +32 -0
  14. package/dist/diagnostics.js +57 -0
  15. package/dist/dom.d.ts +66 -0
  16. package/dist/dom.js +617 -0
  17. package/dist/effectEvent.d.ts +18 -0
  18. package/dist/effectEvent.js +67 -0
  19. package/dist/errors.d.ts +5 -0
  20. package/dist/errors.js +23 -0
  21. package/dist/form.d.ts +19 -0
  22. package/dist/form.js +13 -0
  23. package/dist/index.d.ts +22 -0
  24. package/dist/index.js +21 -0
  25. package/dist/jsx-runtime.d.ts +1 -0
  26. package/dist/jsx-runtime.js +1 -0
  27. package/dist/jsx.d.ts +86 -0
  28. package/dist/jsx.js +1 -0
  29. package/dist/load.d.ts +13 -0
  30. package/dist/load.js +11 -0
  31. package/dist/mount.d.ts +25 -0
  32. package/dist/mount.js +61 -0
  33. package/dist/pages.d.ts +40 -0
  34. package/dist/pages.js +86 -0
  35. package/dist/program.d.ts +48 -0
  36. package/dist/program.js +154 -0
  37. package/dist/query.d.ts +22 -0
  38. package/dist/query.js +12 -0
  39. package/dist/resource.d.ts +34 -0
  40. package/dist/resource.js +59 -0
  41. package/dist/runtime.d.ts +16 -0
  42. package/dist/runtime.js +27 -0
  43. package/dist/session.d.ts +65 -0
  44. package/dist/session.js +186 -0
  45. package/dist/share.d.ts +2 -0
  46. package/dist/share.js +40 -0
  47. package/dist/state.d.ts +2 -0
  48. package/dist/state.js +7 -0
  49. package/dist/task.d.ts +53 -0
  50. package/dist/task.js +70 -0
  51. package/dist/tasks.d.ts +69 -0
  52. package/dist/tasks.js +126 -0
  53. package/dist/testing.d.ts +26 -0
  54. package/dist/testing.js +44 -0
  55. package/package.json +137 -0
package/dist/dom.js ADDED
@@ -0,0 +1,617 @@
1
+ import { runAll, reportError, reportSafely } from './errors.js';
2
+ import { eventEffects } from './effectEvent.js';
3
+ import { traceBinding } from './diagnostics.js';
4
+ import { shareValue } from './share.js';
5
+ import { startMount } from './mount.js';
6
+ const equal = (a, b) => a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
7
+ /** Compiler implementation. No implicit tracking, proxies, or per-binding subscriptions. */
8
+ export class Scope {
9
+ value;
10
+ send;
11
+ report;
12
+ jobs = [];
13
+ cleanups = [];
14
+ disposed = false;
15
+ revision = 0;
16
+ constructor(value, send, report = reportError) {
17
+ this.value = value;
18
+ this.send = send;
19
+ this.report = report;
20
+ }
21
+ derive(dependencies, compute, source) {
22
+ let previous;
23
+ let value;
24
+ let revision = -1;
25
+ return () => {
26
+ if (revision === this.revision)
27
+ return value;
28
+ const next = dependencies();
29
+ if (!previous || !equal(previous, next)) {
30
+ value = previous ? shareValue(value, compute()) : compute();
31
+ traceBinding(source, previous, next, 'derive');
32
+ previous = next;
33
+ }
34
+ revision = this.revision;
35
+ return value;
36
+ };
37
+ }
38
+ watch(dependencies, apply, source) {
39
+ let previous = dependencies();
40
+ apply();
41
+ traceBinding(source, undefined, previous, 'binding');
42
+ if (!previous.length)
43
+ return;
44
+ const run = () => {
45
+ const next = dependencies();
46
+ if (!equal(previous, next)) {
47
+ apply();
48
+ traceBinding(source, previous, next, 'binding');
49
+ previous = next;
50
+ }
51
+ };
52
+ this.jobs.push(run);
53
+ }
54
+ set(value) {
55
+ if (this.disposed)
56
+ return;
57
+ this.value = value;
58
+ this.revision++;
59
+ for (const job of this.jobs) {
60
+ if (this.disposed)
61
+ break;
62
+ try {
63
+ job();
64
+ }
65
+ catch (error) {
66
+ reportSafely(this.report, error);
67
+ }
68
+ }
69
+ }
70
+ dispose() {
71
+ if (this.disposed)
72
+ return;
73
+ this.disposed = true;
74
+ this.jobs.length = 0;
75
+ runAll(this.cleanups.splice(0).reverse(), this.report);
76
+ }
77
+ }
78
+ const contentBrand = Symbol('compiled content');
79
+ /** Compiler marker: declare inside view(), or directly in a compiled component prop. */
80
+ export function slot(_render) {
81
+ throw new Error('MVU slot reached runtime without the snapshot JSX compiler');
82
+ }
83
+ // Placement values are opaque to structural sharing, which only traverses plain data.
84
+ class SlotPlacement {
85
+ definition;
86
+ value;
87
+ [contentBrand] = true;
88
+ constructor(definition, value) {
89
+ this.definition = definition;
90
+ this.value = value;
91
+ }
92
+ }
93
+ function isContent(value) {
94
+ return (value !== null &&
95
+ (typeof value === 'object' || typeof value === 'function') &&
96
+ contentBrand in value);
97
+ }
98
+ /** Compiler output. Captures follow their declaring scope; placements own independent scopes. */
99
+ export function compiledSlot(owner, build) {
100
+ const placements = new Set();
101
+ owner.jobs.push(() => {
102
+ for (const scope of placements)
103
+ scope.set(scope.value);
104
+ });
105
+ owner.cleanups.push(() => {
106
+ for (const scope of placements)
107
+ scope.dispose();
108
+ placements.clear();
109
+ });
110
+ const definition = {
111
+ mount(parent, before, value) {
112
+ if (owner.disposed)
113
+ throw new Error('Cannot mount content after its declaring view disposed');
114
+ const scope = new Scope(value, owner.send, owner.report);
115
+ const fragment = buildFragment(parent);
116
+ const range = markers(fragment, null);
117
+ scope.cleanups.push(() => remove(range.start, range.end));
118
+ try {
119
+ build(scope, fragment, range.end);
120
+ parent.insertBefore(fragment, before);
121
+ placements.add(scope);
122
+ }
123
+ catch (error) {
124
+ scope.dispose();
125
+ throw error;
126
+ }
127
+ return {
128
+ set(value) {
129
+ if (!Object.is(scope.value, value))
130
+ scope.set(value);
131
+ },
132
+ dispose() {
133
+ placements.delete(scope);
134
+ scope.dispose();
135
+ },
136
+ };
137
+ },
138
+ };
139
+ return Object.assign((value) => new SlotPlacement(definition, value), {
140
+ [contentBrand]: true,
141
+ definition,
142
+ });
143
+ }
144
+ /** This declaration is a compiler marker, never a component setup callback. */
145
+ export function view(_render) {
146
+ throw new Error('MVU view reached runtime without the snapshot JSX compiler');
147
+ }
148
+ export function compiled(build) {
149
+ return Object.assign(() => {
150
+ throw new Error('Mount compiled views with mountView or inside another compiled view');
151
+ }, { build });
152
+ }
153
+ function markers(parent, before) {
154
+ const start = document.createComment('');
155
+ const end = document.createComment('');
156
+ parent.insertBefore(start, before);
157
+ parent.insertBefore(end, before);
158
+ return { start, end };
159
+ }
160
+ function remove(start, end) {
161
+ let node = start;
162
+ while (node) {
163
+ const next = node.nextSibling;
164
+ node.parentNode?.removeChild(node);
165
+ if (node === end)
166
+ break;
167
+ node = next;
168
+ }
169
+ }
170
+ function clear(start, end) {
171
+ while (start.nextSibling && start.nextSibling !== end)
172
+ start.parentNode.removeChild(start.nextSibling);
173
+ }
174
+ /** Keep the longest ordered run mounted in place; only insert/move the other rows. */
175
+ function stationaryIndices(order) {
176
+ const tails = [];
177
+ const previous = Array.from({ length: order.length }, () => -1);
178
+ for (let index = 0; index < order.length; index++) {
179
+ if (order[index] < 0)
180
+ continue;
181
+ let low = 0, high = tails.length;
182
+ while (low < high) {
183
+ const middle = (low + high) >>> 1;
184
+ if (order[tails[middle]] < order[index])
185
+ low = middle + 1;
186
+ else
187
+ high = middle;
188
+ }
189
+ previous[index] = low ? tails[low - 1] : -1;
190
+ tails[low] = index;
191
+ }
192
+ const result = new Set();
193
+ for (let index = tails.at(-1) ?? -1; index >= 0; index = previous[index])
194
+ result.add(index);
195
+ return result;
196
+ }
197
+ const svgNamespace = 'http://www.w3.org/2000/svg';
198
+ // Detached regions must retain the insertion context, including foreignObject's
199
+ // switch back to HTML. Weak keys do not retain completed build fragments.
200
+ const fragmentSvg = new WeakMap();
201
+ function svgChildren(parent) {
202
+ return (fragmentSvg.get(parent) ??
203
+ (parent instanceof Element &&
204
+ parent.namespaceURI === svgNamespace &&
205
+ parent.localName !== 'foreignObject'));
206
+ }
207
+ function buildFragment(parent) {
208
+ const fragment = (parent.ownerDocument ?? document).createDocumentFragment();
209
+ fragmentSvg.set(fragment, svgChildren(parent));
210
+ return fragment;
211
+ }
212
+ export function mountView(parent, definition, source, options = {}) {
213
+ const { start, end } = markers(parent, null);
214
+ const scope = new Scope(source.model(), source.send, options.onError);
215
+ let unsubscribe = () => { };
216
+ try {
217
+ const fragment = buildFragment(parent);
218
+ definition.build(scope, fragment, null);
219
+ parent.insertBefore(fragment, end);
220
+ unsubscribe = source.subscribe((model) => scope.set(model));
221
+ }
222
+ catch (error) {
223
+ scope.dispose();
224
+ remove(start, end);
225
+ throw error;
226
+ }
227
+ return () => {
228
+ if (scope.disposed)
229
+ return;
230
+ runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
231
+ };
232
+ }
233
+ export function element(parent, before, tag) {
234
+ const doc = parent.ownerDocument ?? document;
235
+ const node = tag === 'svg' || svgChildren(parent)
236
+ ? doc.createElementNS(svgNamespace, tag)
237
+ : doc.createElement(tag);
238
+ parent.insertBefore(node, before);
239
+ return node;
240
+ }
241
+ const classTokens = new WeakMap();
242
+ const styleProperties = new WeakMap();
243
+ function scalar(value) {
244
+ if (value == null)
245
+ return '';
246
+ if (typeof value === 'string' ||
247
+ typeof value === 'number' ||
248
+ typeof value === 'boolean' ||
249
+ typeof value === 'bigint')
250
+ return String(value);
251
+ throw new Error('DOM values must be scalar. Render objects as compiled child views or domain collections.');
252
+ }
253
+ export function attribute(element, name, value) {
254
+ const key = name === 'className' ? 'class' : name === 'tabIndex' ? 'tabindex' : name;
255
+ if (name === 'classList') {
256
+ const tokens = (value ?? {});
257
+ const next = new Set();
258
+ for (const [classes, enabled] of Object.entries(tokens))
259
+ for (const token of classes.split(/\s+/u).filter(Boolean))
260
+ if (enabled)
261
+ next.add(token);
262
+ for (const token of classTokens.get(element) ?? [])
263
+ if (!next.has(token))
264
+ element.classList.remove(token);
265
+ for (const token of next)
266
+ element.classList.add(token);
267
+ classTokens.set(element, next);
268
+ }
269
+ else if (name === 'style' && value != null && typeof value === 'object' && 'style' in element) {
270
+ const style = element.style;
271
+ const entries = Object.entries(value).map(([property, value]) => [
272
+ property.startsWith('--')
273
+ ? property
274
+ : property.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`),
275
+ scalar(value),
276
+ ]);
277
+ const next = new Set(entries.map(([property]) => property));
278
+ for (const property of styleProperties.get(element) ?? [])
279
+ if (!next.has(property))
280
+ style.removeProperty(property);
281
+ for (const [property, value] of entries)
282
+ style.setProperty(property, value);
283
+ styleProperties.set(element, next);
284
+ }
285
+ else if (name === 'value' && 'value' in element) {
286
+ const next = scalar(value);
287
+ if (element.value !== next)
288
+ element.value = next;
289
+ }
290
+ else if ([
291
+ 'checked',
292
+ 'selected',
293
+ 'disabled',
294
+ 'multiple',
295
+ 'hidden',
296
+ 'autofocus',
297
+ 'controls',
298
+ 'autoplay',
299
+ 'loop',
300
+ 'muted',
301
+ 'playsinline',
302
+ 'readonly',
303
+ 'required',
304
+ 'open',
305
+ 'inert',
306
+ 'download',
307
+ ].includes(name) &&
308
+ typeof value === 'boolean') {
309
+ element.toggleAttribute(name, Boolean(value));
310
+ if (name in element)
311
+ Reflect.set(element, name, Boolean(value));
312
+ }
313
+ else if (value == null ||
314
+ (value === false && !name.startsWith('aria-') && !name.startsWith('data-'))) {
315
+ element.removeAttribute(key);
316
+ }
317
+ else {
318
+ const next = scalar(value);
319
+ if (element.getAttribute(key) !== next)
320
+ element.setAttribute(key, next);
321
+ }
322
+ if (key === 'class')
323
+ for (const token of classTokens.get(element) ?? [])
324
+ element.classList.add(token);
325
+ if (name === 'style' && (value == null || typeof value !== 'object'))
326
+ styleProperties.delete(element);
327
+ }
328
+ export function text(scope, parent, before, dependencies, read, source) {
329
+ const node = document.createTextNode('');
330
+ parent.insertBefore(node, before);
331
+ let start;
332
+ let content;
333
+ scope.watch(dependencies, () => {
334
+ const value = read();
335
+ if (isContent(value)) {
336
+ node.data = '';
337
+ if (content?.definition === value.definition) {
338
+ content.mounted.set(value.value);
339
+ return;
340
+ }
341
+ content?.mounted.dispose();
342
+ content = undefined;
343
+ if (start)
344
+ clear(start, node);
345
+ else {
346
+ start = document.createComment('');
347
+ scope.cleanups.push(() => content?.mounted.dispose());
348
+ node.parentNode.insertBefore(start, node);
349
+ }
350
+ content = {
351
+ definition: value.definition,
352
+ mounted: value.definition.mount(node.parentNode, node, value.value),
353
+ };
354
+ return;
355
+ }
356
+ if (content) {
357
+ content.mounted.dispose();
358
+ content = undefined;
359
+ clear(start, node);
360
+ }
361
+ if (value != null && typeof value === 'object')
362
+ throw new Error('Object rendered as text. Use collection(...).from(items).map(...) for entity lists.');
363
+ const next = value == null || typeof value === 'boolean' ? '' : scalar(value);
364
+ if (node.data !== next)
365
+ node.data = next;
366
+ }, source);
367
+ }
368
+ export function event(scope, element, name, handler) {
369
+ let effects;
370
+ const capture = name.endsWith('Capture');
371
+ const type = name.slice(2, capture ? -7 : undefined).toLowerCase();
372
+ const listener = (event) => {
373
+ if (!scope.disposed) {
374
+ try {
375
+ const result = handler(event);
376
+ if (result !== null && typeof result === 'object' && !scope.disposed) {
377
+ effects ??= eventEffects(scope.report);
378
+ effects.accept(result);
379
+ }
380
+ }
381
+ catch (error) {
382
+ reportSafely(scope.report, error);
383
+ }
384
+ }
385
+ };
386
+ element.addEventListener(type, listener, capture);
387
+ scope.cleanups.push(() => {
388
+ element.removeEventListener(type, listener, capture);
389
+ effects?.dispose();
390
+ });
391
+ }
392
+ export function branch(scope, parent, before, choose, yes, no) {
393
+ const { start, end } = markers(parent, before);
394
+ let child;
395
+ let active;
396
+ const update = () => {
397
+ const next = Boolean(choose());
398
+ if (active !== next) {
399
+ child?.dispose();
400
+ clear(start, end);
401
+ child = new Scope(scope.value, scope.send, scope.report);
402
+ const fragment = buildFragment(end.parentNode);
403
+ try {
404
+ (next ? yes : no)(child, fragment, null);
405
+ end.parentNode.insertBefore(fragment, end);
406
+ active = next;
407
+ }
408
+ catch (error) {
409
+ child.dispose();
410
+ active = undefined;
411
+ throw error;
412
+ }
413
+ }
414
+ else
415
+ child.set(scope.value);
416
+ };
417
+ scope.jobs.push(update);
418
+ scope.cleanups.push(() => child?.dispose());
419
+ update();
420
+ }
421
+ export function each(scope, parent, before, read, outer, indexUsed, build) {
422
+ const end = document.createComment('');
423
+ parent.insertBefore(end, before);
424
+ const rows = new Map();
425
+ let previousList;
426
+ let previousOuter = [];
427
+ let previousIdentities = [];
428
+ const update = () => {
429
+ const target = end.parentNode;
430
+ const next = read();
431
+ const nextOuter = outer();
432
+ const outerChanged = !equal(previousOuter, nextOuter);
433
+ if (next === previousList && !outerChanged)
434
+ return;
435
+ const collection = 'items' in next ? next : undefined;
436
+ const items = collection ? collection.items : next;
437
+ const identities = items.map((item, index) => {
438
+ const key = collection ? collection.identity(item, index) : item;
439
+ if (typeof key !== 'string' && typeof key !== 'number')
440
+ throw new Error('Object lists need domain identity. Declare collection(identity) once; do not add JSX keys.');
441
+ return key;
442
+ });
443
+ if (new Set(identities).size !== identities.length)
444
+ throw new Error('Duplicate collection identity. Identity must be unique within the rendered collection.');
445
+ const keep = new Set(identities);
446
+ for (const [key, row] of rows) {
447
+ if (keep.has(key))
448
+ continue;
449
+ row.scope.dispose();
450
+ remove(row.start, row.end);
451
+ rows.delete(key);
452
+ }
453
+ for (let index = 0; index < items.length; index++) {
454
+ const key = identities[index];
455
+ const row = rows.get(key);
456
+ const item = row ? shareValue(row.scope.value[0], items[index]) : items[index];
457
+ if (!row) {
458
+ const fragment = buildFragment(target);
459
+ const range = markers(fragment, null);
460
+ const child = new Scope([item, index], scope.send, scope.report);
461
+ try {
462
+ build(child, fragment, range.end);
463
+ }
464
+ catch (error) {
465
+ child.dispose();
466
+ throw error;
467
+ }
468
+ // A single element is its own stable range. Keep markers for dynamic/multiple roots.
469
+ const root = range.start.nextSibling;
470
+ const single = root instanceof Element && root.nextSibling === range.end;
471
+ rows.set(key, {
472
+ start: single ? root : range.start,
473
+ end: single ? root : range.end,
474
+ scope: child,
475
+ });
476
+ if (single) {
477
+ range.start.remove();
478
+ range.end.remove();
479
+ }
480
+ target.insertBefore(fragment, end);
481
+ }
482
+ else if (outerChanged ||
483
+ row.scope.value[0] !== item ||
484
+ (indexUsed && row.scope.value[1] !== index)) {
485
+ row.scope.set([item, index]);
486
+ }
487
+ }
488
+ const positions = new Map(previousIdentities.map((key, index) => [key, index]));
489
+ const stationary = stationaryIndices(identities.map((key) => positions.get(key) ?? -1));
490
+ let anchor = end;
491
+ for (let index = identities.length - 1; index >= 0; index--) {
492
+ const row = rows.get(identities[index]);
493
+ if (!stationary.has(index) && row.end.nextSibling !== anchor) {
494
+ const focused = document.activeElement instanceof HTMLElement ? document.activeElement : undefined;
495
+ let node = row.start;
496
+ while (node) {
497
+ const next = node.nextSibling;
498
+ const move = Reflect.get(target, 'moveBefore');
499
+ if (typeof move === 'function' && target.isConnected && node.isConnected)
500
+ move.call(target, node, anchor);
501
+ else
502
+ target.insertBefore(node, anchor);
503
+ if (node === row.end)
504
+ break;
505
+ node = next;
506
+ }
507
+ if (focused?.isConnected && document.activeElement !== focused)
508
+ focused.focus({ preventScroll: true });
509
+ }
510
+ anchor = row.start;
511
+ }
512
+ previousList = next;
513
+ previousOuter = nextOuter;
514
+ previousIdentities = identities;
515
+ };
516
+ scope.jobs.push(update);
517
+ scope.cleanups.push(() => {
518
+ for (const row of rows.values())
519
+ row.scope.dispose();
520
+ rows.clear();
521
+ });
522
+ update();
523
+ }
524
+ export function invoke(scope, parent, before, dependencies, read, build) {
525
+ const child = new Scope(read(), scope.send, scope.report);
526
+ scope.cleanups.push(() => child.dispose());
527
+ build(child, parent, before);
528
+ scope.watch(dependencies, () => {
529
+ const next = read();
530
+ if (!equal(child.value, next))
531
+ child.set(next);
532
+ });
533
+ }
534
+ export function child(scope, parent, before, definition, dependencies, model, send) {
535
+ const child = new Scope(model(), send, scope.report);
536
+ scope.cleanups.push(() => child.dispose());
537
+ definition.build(child, parent, before);
538
+ scope.watch(dependencies, () => {
539
+ const next = model();
540
+ if (!Object.is(child.value, next))
541
+ child.set(next);
542
+ });
543
+ }
544
+ export function attach(scope, element, dependencies, read) {
545
+ let generation = 0;
546
+ let active;
547
+ scope.cleanups.push(() => {
548
+ generation++;
549
+ active?.dispose();
550
+ active = undefined;
551
+ });
552
+ scope.watch(dependencies, () => {
553
+ const mount = read();
554
+ if (mount && active?.update(mount))
555
+ return;
556
+ const token = ++generation;
557
+ active?.dispose();
558
+ active = undefined;
559
+ if (!mount)
560
+ return;
561
+ queueMicrotask(() => {
562
+ if (!scope.disposed && token === generation) {
563
+ try {
564
+ active = startMount(element, mount, scope.report);
565
+ }
566
+ catch (error) {
567
+ reportSafely(scope.report, error);
568
+ }
569
+ }
570
+ });
571
+ });
572
+ }
573
+ export function portal(scope, build) {
574
+ const host = document.createElement('div');
575
+ host.style.display = 'contents';
576
+ document.body.appendChild(host);
577
+ const child = new Scope(scope.value, scope.send, scope.report);
578
+ scope.cleanups.push(() => {
579
+ child.dispose();
580
+ host.remove();
581
+ });
582
+ build(child, host, null);
583
+ scope.jobs.push(() => child.set(scope.value));
584
+ }
585
+ /** Build static markup once per document/namespace, then clone native nodes at each use. */
586
+ export function template(build) {
587
+ const documents = new WeakMap();
588
+ return (parent, before) => {
589
+ const doc = parent.ownerDocument ?? document;
590
+ const svg = svgChildren(parent);
591
+ let variants = documents.get(doc);
592
+ if (!variants) {
593
+ variants = new Map();
594
+ documents.set(doc, variants);
595
+ }
596
+ let fragment = variants.get(svg);
597
+ if (!fragment) {
598
+ const host = svg
599
+ ? doc.createElementNS('http://www.w3.org/2000/svg', 'svg')
600
+ : doc.createElement('div');
601
+ build(host, null);
602
+ if (host.firstChild && !host.firstChild.nextSibling) {
603
+ fragment = host.removeChild(host.firstChild);
604
+ }
605
+ else {
606
+ fragment = doc.createDocumentFragment();
607
+ while (host.firstChild)
608
+ fragment.appendChild(host.firstChild);
609
+ }
610
+ variants.set(svg, fragment);
611
+ }
612
+ parent.insertBefore(fragment.cloneNode(true), before);
613
+ };
614
+ }
615
+ export function literal(parent, before, value) {
616
+ parent.insertBefore((parent.ownerDocument ?? document).createTextNode(value), before);
617
+ }
@@ -0,0 +1,18 @@
1
+ import { Effect } from 'effect';
2
+ import { type ReportError } from './errors.js';
3
+ import { type UiRuntime } from './runtime.js';
4
+ declare const tag: unique symbol;
5
+ export interface EffectEventRequest {
6
+ readonly [tag]: true;
7
+ readonly policy: 'drop' | 'replace';
8
+ readonly effect: Effect.Effect<unknown, unknown>;
9
+ }
10
+ /** Capture/prevent native events on every call; the policy gates only Effect execution. */
11
+ export declare function effectEvent<EventType extends Event, E>(policy: 'drop' | 'replace', load: (event: EventType) => Effect.Effect<unknown, E>): (event: EventType) => EffectEventRequest;
12
+ 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
+ /** Internal listener owner, allocated only when an event returns a value needing inspection. */
14
+ export declare function eventEffects(report: ReportError): {
15
+ accept(value: unknown): void;
16
+ dispose(): void;
17
+ };
18
+ export {};
@@ -0,0 +1,67 @@
1
+ import { Effect, Fiber } from 'effect';
2
+ import { reportSafely } from './errors.js';
3
+ import { defaultUiRuntime } from './runtime.js';
4
+ const tag = Symbol('Effect event');
5
+ export function effectEvent(policy, load, runtime) {
6
+ const owner = runtime ?? defaultUiRuntime;
7
+ return (event) => ({ [tag]: true, policy, effect: owner.provide(load(event)) });
8
+ }
9
+ /** Internal listener owner, allocated only when an event returns a value needing inspection. */
10
+ export function eventEffects(report) {
11
+ let active;
12
+ let disposed = false;
13
+ const stop = () => {
14
+ const previous = active;
15
+ active = undefined;
16
+ if (previous?.fiber)
17
+ Effect.runFork(Fiber.interrupt(previous.fiber));
18
+ };
19
+ return {
20
+ accept(value) {
21
+ if (disposed)
22
+ return;
23
+ if (value && typeof value === 'object' && tag in value) {
24
+ const request = value;
25
+ if (request.policy === 'drop' && active)
26
+ return;
27
+ stop();
28
+ const token = {};
29
+ active = token;
30
+ try {
31
+ const fiber = Effect.runFork(request.effect);
32
+ token.fiber = fiber;
33
+ if (disposed || active !== token) {
34
+ Effect.runFork(Fiber.interrupt(fiber));
35
+ return;
36
+ }
37
+ fiber.addObserver((exit) => {
38
+ if (disposed || active !== token)
39
+ return;
40
+ active = undefined;
41
+ if (exit._tag === 'Failure')
42
+ reportSafely(report, exit.cause);
43
+ });
44
+ }
45
+ catch (error) {
46
+ if (active === token)
47
+ active = undefined;
48
+ reportSafely(report, error);
49
+ }
50
+ }
51
+ else if (Effect.isEffect(value)) {
52
+ reportSafely(report, new Error('Event returned an unowned Effect. Use effectEvent(policy, factory) or dispatch a command.'));
53
+ }
54
+ else if (value &&
55
+ typeof value === 'object' &&
56
+ 'then' in value &&
57
+ typeof value.then === 'function') {
58
+ void Promise.resolve(value).catch((error) => reportSafely(report, error));
59
+ reportSafely(report, new Error('Event returned an unowned Promise. Adapt it with fromPromise inside effectEvent or a command.'));
60
+ }
61
+ },
62
+ dispose() {
63
+ disposed = true;
64
+ stop();
65
+ },
66
+ };
67
+ }