foldkit 0.101.0 → 0.102.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 (208) hide show
  1. package/README.md +2 -1
  2. package/dist/canvas/view.d.ts +1 -1
  3. package/dist/canvas/view.d.ts.map +1 -1
  4. package/dist/canvas/view.js +5 -5
  5. package/dist/command/index.d.ts +71 -0
  6. package/dist/command/index.d.ts.map +1 -1
  7. package/dist/command/index.js +34 -1
  8. package/dist/command/public.d.ts +1 -1
  9. package/dist/command/public.d.ts.map +1 -1
  10. package/dist/command/public.js +1 -1
  11. package/dist/devTools/overlay.d.ts.map +1 -1
  12. package/dist/devTools/overlay.js +137 -110
  13. package/dist/dom/dom.d.ts +8 -11
  14. package/dist/dom/dom.d.ts.map +1 -1
  15. package/dist/dom/dom.js +8 -11
  16. package/dist/dom/elementMovement.d.ts +1 -3
  17. package/dist/dom/elementMovement.d.ts.map +1 -1
  18. package/dist/dom/elementMovement.js +1 -3
  19. package/dist/dom/inert.d.ts +2 -4
  20. package/dist/dom/inert.d.ts.map +1 -1
  21. package/dist/dom/inert.js +2 -4
  22. package/dist/dom/scrollLock.d.ts +2 -2
  23. package/dist/dom/scrollLock.js +2 -2
  24. package/dist/dom/waitForAnimation.d.ts +1 -1
  25. package/dist/dom/waitForAnimation.js +1 -1
  26. package/dist/html/boundary.d.ts +98 -0
  27. package/dist/html/boundary.d.ts.map +1 -0
  28. package/dist/html/boundary.js +176 -0
  29. package/dist/html/childAttribute.d.ts +44 -0
  30. package/dist/html/childAttribute.d.ts.map +1 -0
  31. package/dist/html/childAttribute.js +34 -0
  32. package/dist/html/index.d.ts +70 -23
  33. package/dist/html/index.d.ts.map +1 -1
  34. package/dist/html/index.js +639 -575
  35. package/dist/html/lazy.d.ts +12 -7
  36. package/dist/html/lazy.d.ts.map +1 -1
  37. package/dist/html/lazy.js +30 -11
  38. package/dist/html/public.d.ts +2 -2
  39. package/dist/html/public.d.ts.map +1 -1
  40. package/dist/html/public.js +1 -1
  41. package/dist/html/runtimeSingleton.d.ts +72 -0
  42. package/dist/html/runtimeSingleton.d.ts.map +1 -0
  43. package/dist/html/runtimeSingleton.js +112 -0
  44. package/dist/html/submodel.d.ts +98 -0
  45. package/dist/html/submodel.d.ts.map +1 -0
  46. package/dist/html/submodel.js +190 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  50. package/dist/render/render.d.ts +1 -1
  51. package/dist/render/render.js +1 -1
  52. package/dist/runtime/messagePriority.d.ts +5 -1
  53. package/dist/runtime/messagePriority.d.ts.map +1 -1
  54. package/dist/runtime/messagePriority.js +25 -4
  55. package/dist/runtime/runtime.d.ts +1 -1
  56. package/dist/runtime/runtime.d.ts.map +1 -1
  57. package/dist/runtime/runtime.js +115 -61
  58. package/dist/submodel/public.d.ts +4 -0
  59. package/dist/submodel/public.d.ts.map +1 -0
  60. package/dist/submodel/public.js +1 -0
  61. package/dist/submodel/submodel.d.ts +32 -0
  62. package/dist/submodel/submodel.d.ts.map +1 -0
  63. package/dist/submodel/submodel.js +1 -0
  64. package/dist/test/apps/disabledButton.d.ts +4 -5
  65. package/dist/test/apps/disabledButton.d.ts.map +1 -1
  66. package/dist/test/apps/disabledButton.js +16 -16
  67. package/dist/test/scene.d.ts +8 -8
  68. package/dist/test/scene.d.ts.map +1 -1
  69. package/dist/test/scene.js +25 -13
  70. package/dist/test/story.d.ts +15 -8
  71. package/dist/test/story.d.ts.map +1 -1
  72. package/dist/test/story.js +21 -9
  73. package/dist/ui/animation/index.d.ts +30 -14
  74. package/dist/ui/animation/index.d.ts.map +1 -1
  75. package/dist/ui/animation/index.js +9 -19
  76. package/dist/ui/animation/public.d.ts +2 -2
  77. package/dist/ui/animation/public.d.ts.map +1 -1
  78. package/dist/ui/animation/public.js +1 -1
  79. package/dist/ui/calendar/index.d.ts +199 -84
  80. package/dist/ui/calendar/index.d.ts.map +1 -1
  81. package/dist/ui/calendar/index.js +129 -140
  82. package/dist/ui/calendar/public.d.ts +2 -2
  83. package/dist/ui/calendar/public.d.ts.map +1 -1
  84. package/dist/ui/calendar/public.js +1 -1
  85. package/dist/ui/checkbox/index.d.ts +93 -21
  86. package/dist/ui/checkbox/index.d.ts.map +1 -1
  87. package/dist/ui/checkbox/index.js +62 -33
  88. package/dist/ui/checkbox/public.d.ts +2 -2
  89. package/dist/ui/checkbox/public.d.ts.map +1 -1
  90. package/dist/ui/checkbox/public.js +1 -1
  91. package/dist/ui/combobox/multi.d.ts +35 -91
  92. package/dist/ui/combobox/multi.d.ts.map +1 -1
  93. package/dist/ui/combobox/multi.js +34 -17
  94. package/dist/ui/combobox/multiPublic.d.ts +2 -2
  95. package/dist/ui/combobox/multiPublic.d.ts.map +1 -1
  96. package/dist/ui/combobox/multiPublic.js +1 -1
  97. package/dist/ui/combobox/public.d.ts +3 -3
  98. package/dist/ui/combobox/public.d.ts.map +1 -1
  99. package/dist/ui/combobox/public.js +2 -2
  100. package/dist/ui/combobox/shared.d.ts +56 -31
  101. package/dist/ui/combobox/shared.d.ts.map +1 -1
  102. package/dist/ui/combobox/shared.js +333 -322
  103. package/dist/ui/combobox/single.d.ts +46 -93
  104. package/dist/ui/combobox/single.d.ts.map +1 -1
  105. package/dist/ui/combobox/single.js +44 -17
  106. package/dist/ui/datePicker/index.d.ts +256 -48
  107. package/dist/ui/datePicker/index.d.ts.map +1 -1
  108. package/dist/ui/datePicker/index.js +149 -104
  109. package/dist/ui/datePicker/public.d.ts +2 -2
  110. package/dist/ui/datePicker/public.d.ts.map +1 -1
  111. package/dist/ui/datePicker/public.js +1 -1
  112. package/dist/ui/dialog/index.d.ts +95 -39
  113. package/dist/ui/dialog/index.d.ts.map +1 -1
  114. package/dist/ui/dialog/index.js +71 -62
  115. package/dist/ui/dialog/public.d.ts +2 -2
  116. package/dist/ui/dialog/public.d.ts.map +1 -1
  117. package/dist/ui/dialog/public.js +1 -1
  118. package/dist/ui/disclosure/index.d.ts +71 -31
  119. package/dist/ui/disclosure/index.d.ts.map +1 -1
  120. package/dist/ui/disclosure/index.js +57 -62
  121. package/dist/ui/disclosure/public.d.ts +2 -2
  122. package/dist/ui/disclosure/public.d.ts.map +1 -1
  123. package/dist/ui/disclosure/public.js +1 -1
  124. package/dist/ui/dragAndDrop/index.d.ts +6 -6
  125. package/dist/ui/dragAndDrop/index.d.ts.map +1 -1
  126. package/dist/ui/dragAndDrop/index.js +7 -7
  127. package/dist/ui/dragAndDrop/public.d.ts +1 -1
  128. package/dist/ui/dragAndDrop/public.d.ts.map +1 -1
  129. package/dist/ui/dragAndDrop/public.js +1 -1
  130. package/dist/ui/fileDrop/index.d.ts +42 -46
  131. package/dist/ui/fileDrop/index.d.ts.map +1 -1
  132. package/dist/ui/fileDrop/index.js +30 -46
  133. package/dist/ui/fileDrop/public.d.ts +2 -2
  134. package/dist/ui/fileDrop/public.d.ts.map +1 -1
  135. package/dist/ui/fileDrop/public.js +1 -1
  136. package/dist/ui/listbox/multi.d.ts +39 -84
  137. package/dist/ui/listbox/multi.d.ts.map +1 -1
  138. package/dist/ui/listbox/multi.js +38 -20
  139. package/dist/ui/listbox/multiPublic.d.ts +2 -2
  140. package/dist/ui/listbox/multiPublic.d.ts.map +1 -1
  141. package/dist/ui/listbox/multiPublic.js +1 -1
  142. package/dist/ui/listbox/public.d.ts +3 -3
  143. package/dist/ui/listbox/public.d.ts.map +1 -1
  144. package/dist/ui/listbox/public.js +2 -2
  145. package/dist/ui/listbox/shared.d.ts +71 -30
  146. package/dist/ui/listbox/shared.d.ts.map +1 -1
  147. package/dist/ui/listbox/shared.js +319 -296
  148. package/dist/ui/listbox/single.d.ts +57 -85
  149. package/dist/ui/listbox/single.d.ts.map +1 -1
  150. package/dist/ui/listbox/single.js +48 -24
  151. package/dist/ui/menu/index.d.ts +80 -36
  152. package/dist/ui/menu/index.d.ts.map +1 -1
  153. package/dist/ui/menu/index.js +117 -86
  154. package/dist/ui/menu/public.d.ts +2 -2
  155. package/dist/ui/menu/public.d.ts.map +1 -1
  156. package/dist/ui/menu/public.js +1 -1
  157. package/dist/ui/popover/index.d.ts +117 -44
  158. package/dist/ui/popover/index.d.ts.map +1 -1
  159. package/dist/ui/popover/index.js +88 -101
  160. package/dist/ui/popover/public.d.ts +2 -2
  161. package/dist/ui/popover/public.d.ts.map +1 -1
  162. package/dist/ui/popover/public.js +1 -1
  163. package/dist/ui/radioGroup/index.d.ts +122 -45
  164. package/dist/ui/radioGroup/index.d.ts.map +1 -1
  165. package/dist/ui/radioGroup/index.js +111 -72
  166. package/dist/ui/radioGroup/public.d.ts +2 -2
  167. package/dist/ui/radioGroup/public.d.ts.map +1 -1
  168. package/dist/ui/radioGroup/public.js +1 -1
  169. package/dist/ui/slider/index.d.ts +72 -34
  170. package/dist/ui/slider/index.d.ts.map +1 -1
  171. package/dist/ui/slider/index.js +40 -49
  172. package/dist/ui/slider/public.d.ts +2 -2
  173. package/dist/ui/slider/public.d.ts.map +1 -1
  174. package/dist/ui/slider/public.js +1 -1
  175. package/dist/ui/switch/index.d.ts +74 -21
  176. package/dist/ui/switch/index.d.ts.map +1 -1
  177. package/dist/ui/switch/index.js +62 -33
  178. package/dist/ui/switch/public.d.ts +2 -2
  179. package/dist/ui/switch/public.d.ts.map +1 -1
  180. package/dist/ui/switch/public.js +1 -1
  181. package/dist/ui/tabs/index.d.ts +107 -45
  182. package/dist/ui/tabs/index.d.ts.map +1 -1
  183. package/dist/ui/tabs/index.js +99 -81
  184. package/dist/ui/tabs/public.d.ts +2 -2
  185. package/dist/ui/tabs/public.d.ts.map +1 -1
  186. package/dist/ui/tabs/public.js +1 -1
  187. package/dist/ui/toast/index.d.ts +93 -109
  188. package/dist/ui/toast/index.d.ts.map +1 -1
  189. package/dist/ui/toast/index.js +16 -29
  190. package/dist/ui/toast/schema.d.ts +15 -4
  191. package/dist/ui/toast/schema.d.ts.map +1 -1
  192. package/dist/ui/toast/schema.js +11 -4
  193. package/dist/ui/toast/update.d.ts +36 -18
  194. package/dist/ui/toast/update.d.ts.map +1 -1
  195. package/dist/ui/toast/update.js +33 -14
  196. package/dist/ui/tooltip/index.d.ts +94 -42
  197. package/dist/ui/tooltip/index.d.ts.map +1 -1
  198. package/dist/ui/tooltip/index.js +64 -73
  199. package/dist/ui/tooltip/public.d.ts +2 -2
  200. package/dist/ui/tooltip/public.d.ts.map +1 -1
  201. package/dist/ui/tooltip/public.js +1 -1
  202. package/dist/ui/virtualList/index.d.ts +18 -41
  203. package/dist/ui/virtualList/index.d.ts.map +1 -1
  204. package/dist/ui/virtualList/index.js +17 -37
  205. package/dist/ui/virtualList/public.d.ts +2 -2
  206. package/dist/ui/virtualList/public.d.ts.map +1 -1
  207. package/dist/ui/virtualList/public.js +1 -1
  208. package/package.json +1 -1
@@ -1,25 +1,30 @@
1
- import type { Html } from './index.js';
1
+ import type { VNode } from '../vdom.js';
2
2
  /** Creates a memoization slot for a view function. On each render, if the
3
- * function reference, dispatch context, and all arguments are referentially
4
- * equal (`===`) to the previous call, the cached VNode is returned without
3
+ * function reference, dispatch, and all arguments are referentially equal
4
+ * (`===`) to the previous call, the cached VNode is returned without
5
5
  * re-running the view function. Snabbdom's `patchVnode` short-circuits when
6
6
  * it sees the same VNode reference, so both VNode construction and subtree
7
7
  * diffing are skipped.
8
8
  *
9
+ * Dispatch is part of the cache key because event handlers in the cached
10
+ * VNode close over the dispatch active when the VNode was built. Returning
11
+ * a VNode built under a different dispatch would silently misroute every
12
+ * event from that subtree.
13
+ *
9
14
  * The cached VNode must be rendered at a single position in the tree.
10
15
  * Snabbdom tracks the real DOM through each VNode's mutable `.elm` field
11
16
  * and assumes one VNode per position. Rendering the same cached VNode at
12
17
  * two positions causes patches to collide and can duplicate or misplace
13
18
  * DOM nodes. If the same content needs to appear in multiple positions,
14
19
  * create one slot per position. */
15
- export declare const createLazy: () => (<Args extends ReadonlyArray<unknown>>(fn: (...args: Args) => Html, args: Args) => Html);
20
+ export declare const createLazy: () => (<Args extends ReadonlyArray<unknown>>(fn: (...args: Args) => VNode | null, args: Args) => VNode | null);
16
21
  /** Creates a keyed memoization map for view functions rendered in a loop. Each
17
22
  * key gets its own independent cache slot. On each render, only entries whose
18
- * function reference, dispatch context, or arguments have changed by reference
19
- * are recomputed.
23
+ * function reference, dispatch, or arguments have changed by reference are
24
+ * recomputed.
20
25
  *
21
26
  * Like `createLazy`, each key's cached VNode must be rendered at a single
22
27
  * position in the tree. If the same item needs to appear in multiple
23
28
  * positions, create one keyed lazy per position. */
24
- export declare const createKeyedLazy: () => (<Args extends ReadonlyArray<unknown>>(key: string, fn: (...args: Args) => Html, args: Args) => Html);
29
+ export declare const createKeyedLazy: () => (<Args extends ReadonlyArray<unknown>>(key: string, fn: (...args: Args) => VNode | null, args: Args) => VNode | null);
25
30
  //# sourceMappingURL=lazy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lazy.d.ts","sourceRoot":"","sources":["../../src/html/lazy.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAsCtC;;;;;;;;;;;;oCAYoC;AACpC,eAAO,MAAM,UAAU,QAAO,CAAC,CAAC,IAAI,SAAS,aAAa,CAAC,OAAO,CAAC,EACjE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,EAC3B,IAAI,EAAE,IAAI,KACP,IAAI,CAUR,CAAA;AAED;;;;;;;qDAOqD;AACrD,eAAO,MAAM,eAAe,QAAO,CAAC,CAAC,IAAI,SAAS,aAAa,CAAC,OAAO,CAAC,EACtE,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,EAC3B,IAAI,EAAE,IAAI,KACP,IAAI,CAWR,CAAA"}
1
+ {"version":3,"file":"lazy.d.ts","sourceRoot":"","sources":["../../src/html/lazy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAiEvC;;;;;;;;;;;;;;;;;oCAiBoC;AACpC,eAAO,MAAM,UAAU,QAAO,CAAC,CAAC,IAAI,SAAS,aAAa,CAAC,OAAO,CAAC,EACjE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,KAAK,GAAG,IAAI,EACnC,IAAI,EAAE,IAAI,KACP,KAAK,GAAG,IAAI,CAUhB,CAAA;AAED;;;;;;;qDAOqD;AACrD,eAAO,MAAM,eAAe,QAAO,CAAC,CAAC,IAAI,SAAS,aAAa,CAAC,OAAO,CAAC,EACtE,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,KAAK,GAAG,IAAI,EACnC,IAAI,EAAE,IAAI,KACP,KAAK,GAAG,IAAI,CAWhB,CAAA"}
package/dist/html/lazy.js CHANGED
@@ -1,26 +1,45 @@
1
- import { Effect, Predicate } from 'effect';
2
- import { Dispatch } from '../runtime/index.js';
1
+ import { Predicate } from 'effect';
2
+ import { beginLazyTracking, endLazyTracking, markSeenForLazyHit, } from './boundary.js';
3
+ import { requireBoundary, requireDispatch, } from './runtimeSingleton.js';
3
4
  const argsEqual = (previous, current) => previous.length === current.length &&
4
5
  previous.every((value, index) => value === current[index]);
5
- const resolveOrCache = (previousEntry, fn, args, onCache) => Effect.gen(function* () {
6
- const dispatch = yield* Dispatch;
6
+ const resolveOrCache = (previousEntry, fn, args, onCache) => {
7
+ const dispatch = requireDispatch();
8
+ const { registry } = requireBoundary();
9
+ // NOTE: dispatch identity in the cache key matters for the DevTools
10
+ // jumpTo path: a replay render installs `noOpDispatch`, and without
11
+ // this check a subsequent live render could return a vnode whose
12
+ // handlers still reference the noOp.
7
13
  if (Predicate.isNotUndefined(previousEntry) &&
8
14
  previousEntry.fn === fn &&
9
15
  previousEntry.dispatch === dispatch &&
10
16
  argsEqual(previousEntry.args, args)) {
17
+ markSeenForLazyHit(registry, previousEntry.trackedBoundaries);
11
18
  return previousEntry.vnode;
12
19
  }
13
- const vnode = yield* fn(...args);
14
- onCache({ fn, args, dispatch, vnode });
20
+ const trackedBoundaries = beginLazyTracking(registry);
21
+ let vnode;
22
+ try {
23
+ vnode = fn(...args);
24
+ }
25
+ finally {
26
+ endLazyTracking(registry);
27
+ }
28
+ onCache({ fn, args, dispatch, vnode, trackedBoundaries });
15
29
  return vnode;
16
- });
30
+ };
17
31
  /** Creates a memoization slot for a view function. On each render, if the
18
- * function reference, dispatch context, and all arguments are referentially
19
- * equal (`===`) to the previous call, the cached VNode is returned without
32
+ * function reference, dispatch, and all arguments are referentially equal
33
+ * (`===`) to the previous call, the cached VNode is returned without
20
34
  * re-running the view function. Snabbdom's `patchVnode` short-circuits when
21
35
  * it sees the same VNode reference, so both VNode construction and subtree
22
36
  * diffing are skipped.
23
37
  *
38
+ * Dispatch is part of the cache key because event handlers in the cached
39
+ * VNode close over the dispatch active when the VNode was built. Returning
40
+ * a VNode built under a different dispatch would silently misroute every
41
+ * event from that subtree.
42
+ *
24
43
  * The cached VNode must be rendered at a single position in the tree.
25
44
  * Snabbdom tracks the real DOM through each VNode's mutable `.elm` field
26
45
  * and assumes one VNode per position. Rendering the same cached VNode at
@@ -35,8 +54,8 @@ export const createLazy = () => {
35
54
  };
36
55
  /** Creates a keyed memoization map for view functions rendered in a loop. Each
37
56
  * key gets its own independent cache slot. On each render, only entries whose
38
- * function reference, dispatch context, or arguments have changed by reference
39
- * are recomputed.
57
+ * function reference, dispatch, or arguments have changed by reference are
58
+ * recomputed.
40
59
  *
41
60
  * Like `createLazy`, each key's cached VNode must be rendered at a single
42
61
  * position in the tree. If the same item needs to appear in multiple
@@ -1,3 +1,3 @@
1
- export { createKeyedLazy, createLazy, html } from './index.js';
2
- export type { Attribute, Document, Html, KeyboardModifiers, TagName, } from './index.js';
1
+ export { childAttributes, createKeyedLazy, createLazy, html, submodel, } from './index.js';
2
+ export type { Attribute, ChildAttribute, Document, Html, KeyboardModifiers, TagName, } from './index.js';
3
3
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/html/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAE9D,YAAY,EACV,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,iBAAiB,EACjB,OAAO,GACR,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/html/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,eAAe,EACf,UAAU,EACV,IAAI,EACJ,QAAQ,GACT,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,SAAS,EACT,cAAc,EACd,QAAQ,EACR,IAAI,EACJ,iBAAiB,EACjB,OAAO,GACR,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- export { createKeyedLazy, createLazy, html } from './index.js';
1
+ export { childAttributes, createKeyedLazy, createLazy, html, submodel, } from './index.js';
@@ -0,0 +1,72 @@
1
+ import type { Context } from 'effect';
2
+ import { type BoundaryId, type BoundaryRegistry } from './boundary.js';
3
+ /** Synchronous message dispatcher provided to view-time element constructors. */
4
+ export type DispatchSync = (message: unknown) => void;
5
+ export type Frame = Readonly<{
6
+ outerDispatch: DispatchSync;
7
+ runtimeContext: Context.Context<never>;
8
+ boundaryRegistry: BoundaryRegistry;
9
+ boundaryId: BoundaryId;
10
+ }>;
11
+ /** Pushes a new dispatch and runtime context onto the singleton stack. The
12
+ * runtime calls this before invoking a user `view`, and any test or
13
+ * framework helper that builds VNodes outside of a normal render uses the
14
+ * same pair. Nested calls are supported: each push must be matched by a
15
+ * paired {@link clearRuntime} so the previous frame is restored.
16
+ *
17
+ * Optionally accepts a {@link BoundaryRegistry}; the runtime supplies the
18
+ * same registry across renders so Submodel wrap descriptors persist. When
19
+ * omitted (e.g. crash views, test helpers that don't use Submodels), a
20
+ * fresh empty registry is created. The frame starts in the root
21
+ * boundary. */
22
+ export declare const setRuntime: (dispatch: DispatchSync, runtimeContext: Context.Context<never>, boundaryRegistry?: BoundaryRegistry) => void;
23
+ /** Pushes a new frame that inherits the current frame's dispatch, context,
24
+ * and registry, but uses a different boundary. Used by `h.submodel` to
25
+ * enter a child Submodel's wrapping context. Must be paired with
26
+ * {@link clearRuntime}. */
27
+ export declare const pushBoundary: (boundaryId: BoundaryId) => void;
28
+ /** Pushes a fully-specified frame, ignoring whatever is currently on the
29
+ * stack. Used by `h.submodel`'s slot-callback wrapping so the wrapped
30
+ * callback runs in the OUTER (parent) Submodel's context, regardless of
31
+ * what frame is active at the time the callback is invoked.
32
+ *
33
+ * Unlike {@link pushBoundary} (which inherits dispatch/registry/context
34
+ * from the current top), this lets the caller capture a full frame
35
+ * snapshot at one point and replay it later. Without this primitive, a
36
+ * slot callback invoked from a deferred context (setTimeout, Promise,
37
+ * stored callback) would inherit from whatever render's frame happens
38
+ * to be on the stack at the time, silently mis-binding dispatch and
39
+ * registry. Must be paired with {@link clearRuntime}. */
40
+ export declare const pushFrame: (frame: Frame) => void;
41
+ /** Pops the current frame, restoring whatever frame was previously active.
42
+ * Must be paired with a {@link setRuntime} or {@link pushBoundary} on the
43
+ * same call stack, including via `try`/`finally` so an exception inside
44
+ * view code does not leak the frame to subsequent renders.
45
+ *
46
+ * Throws when called on an empty stack. That signals an unmatched
47
+ * push/pop pair somewhere upstream and would silently corrupt later
48
+ * renders if it slid by. */
49
+ export declare const clearRuntime: () => void;
50
+ /** Returns the current dispatcher. For frames in the root boundary, this
51
+ * is the runtime's actual dispatch; for nested Submodel boundaries, this
52
+ * is a cached per-boundary dispatcher that applies the wrapping chain at
53
+ * event-fire time. Throws when called outside of a runtime frame. */
54
+ export declare const requireDispatch: () => DispatchSync;
55
+ /** Returns the current runtime Effect Context, used by Mount integrations
56
+ * that fork message-producing Effects against the live runtime services. */
57
+ export declare const requireRuntimeContext: () => Context.Context<never>;
58
+ /** Returns the current frame's boundary registry and boundary id. Used by
59
+ * `h.submodel` to register wrapping descriptors and compute child
60
+ * boundary ids. Throws when called outside of a runtime frame. */
61
+ export declare const requireBoundary: () => Readonly<{
62
+ registry: BoundaryRegistry;
63
+ boundaryId: BoundaryId;
64
+ }>;
65
+ /** Returns the full current frame, used by `h.submodel` to snapshot the
66
+ * parent's context before pushing a child boundary. The snapshot is
67
+ * captured into slot-callback closures so they can replay the parent's
68
+ * full frame via {@link pushFrame} when invoked, instead of inheriting
69
+ * from whatever happens to be on the stack at invocation time. Throws
70
+ * when called outside of a runtime frame. */
71
+ export declare const getCurrentFrame: () => Frame;
72
+ //# sourceMappingURL=runtimeSingleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtimeSingleton.d.ts","sourceRoot":"","sources":["../../src/html/runtimeSingleton.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,OAAO,EACL,KAAK,UAAU,EACf,KAAK,gBAAgB,EAItB,MAAM,eAAe,CAAA;AAEtB,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;AAErD,MAAM,MAAM,KAAK,GAAG,QAAQ,CAAC;IAC3B,aAAa,EAAE,YAAY,CAAA;IAC3B,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACtC,gBAAgB,EAAE,gBAAgB,CAAA;IAClC,UAAU,EAAE,UAAU,CAAA;CACvB,CAAC,CAAA;AAIF;;;;;;;;;;gBAUgB;AAChB,eAAO,MAAM,UAAU,GACrB,UAAU,YAAY,EACtB,gBAAgB,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EACtC,mBAAkB,gBAA2C,KAC5D,IAOF,CAAA;AAED;;;4BAG4B;AAC5B,eAAO,MAAM,YAAY,GAAI,YAAY,UAAU,KAAG,IAarD,CAAA;AAED;;;;;;;;;;;0DAW0D;AAC1D,eAAO,MAAM,SAAS,GAAI,OAAO,KAAK,KAAG,IAExC,CAAA;AAED;;;;;;;6BAO6B;AAC7B,eAAO,MAAM,YAAY,QAAO,IAU/B,CAAA;AAED;;;sEAGsE;AACtE,eAAO,MAAM,eAAe,QAAO,YAYlC,CAAA;AAED;6EAC6E;AAC7E,eAAO,MAAM,qBAAqB,QAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAQ7D,CAAA;AAED;;mEAEmE;AACnE,eAAO,MAAM,eAAe,QAAO,QAAQ,CAAC;IAC1C,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,UAAU,EAAE,UAAU,CAAA;CACvB,CAQA,CAAA;AAED;;;;;8CAK8C;AAC9C,eAAO,MAAM,eAAe,QAAO,KAQlC,CAAA"}
@@ -0,0 +1,112 @@
1
+ import { ROOT_BOUNDARY, createBoundaryRegistry, getOrCreateBoundaryDispatch, } from './boundary.js';
2
+ const stack = [];
3
+ /** Pushes a new dispatch and runtime context onto the singleton stack. The
4
+ * runtime calls this before invoking a user `view`, and any test or
5
+ * framework helper that builds VNodes outside of a normal render uses the
6
+ * same pair. Nested calls are supported: each push must be matched by a
7
+ * paired {@link clearRuntime} so the previous frame is restored.
8
+ *
9
+ * Optionally accepts a {@link BoundaryRegistry}; the runtime supplies the
10
+ * same registry across renders so Submodel wrap descriptors persist. When
11
+ * omitted (e.g. crash views, test helpers that don't use Submodels), a
12
+ * fresh empty registry is created. The frame starts in the root
13
+ * boundary. */
14
+ export const setRuntime = (dispatch, runtimeContext, boundaryRegistry = createBoundaryRegistry()) => {
15
+ stack.push({
16
+ outerDispatch: dispatch,
17
+ runtimeContext,
18
+ boundaryRegistry,
19
+ boundaryId: ROOT_BOUNDARY,
20
+ });
21
+ };
22
+ /** Pushes a new frame that inherits the current frame's dispatch, context,
23
+ * and registry, but uses a different boundary. Used by `h.submodel` to
24
+ * enter a child Submodel's wrapping context. Must be paired with
25
+ * {@link clearRuntime}. */
26
+ export const pushBoundary = (boundaryId) => {
27
+ const parent = stack[stack.length - 1];
28
+ if (parent === undefined) {
29
+ throw new Error('Foldkit: pushBoundary called without an active runtime frame');
30
+ }
31
+ stack.push({
32
+ outerDispatch: parent.outerDispatch,
33
+ runtimeContext: parent.runtimeContext,
34
+ boundaryRegistry: parent.boundaryRegistry,
35
+ boundaryId,
36
+ });
37
+ };
38
+ /** Pushes a fully-specified frame, ignoring whatever is currently on the
39
+ * stack. Used by `h.submodel`'s slot-callback wrapping so the wrapped
40
+ * callback runs in the OUTER (parent) Submodel's context, regardless of
41
+ * what frame is active at the time the callback is invoked.
42
+ *
43
+ * Unlike {@link pushBoundary} (which inherits dispatch/registry/context
44
+ * from the current top), this lets the caller capture a full frame
45
+ * snapshot at one point and replay it later. Without this primitive, a
46
+ * slot callback invoked from a deferred context (setTimeout, Promise,
47
+ * stored callback) would inherit from whatever render's frame happens
48
+ * to be on the stack at the time, silently mis-binding dispatch and
49
+ * registry. Must be paired with {@link clearRuntime}. */
50
+ export const pushFrame = (frame) => {
51
+ stack.push(frame);
52
+ };
53
+ /** Pops the current frame, restoring whatever frame was previously active.
54
+ * Must be paired with a {@link setRuntime} or {@link pushBoundary} on the
55
+ * same call stack, including via `try`/`finally` so an exception inside
56
+ * view code does not leak the frame to subsequent renders.
57
+ *
58
+ * Throws when called on an empty stack. That signals an unmatched
59
+ * push/pop pair somewhere upstream and would silently corrupt later
60
+ * renders if it slid by. */
61
+ export const clearRuntime = () => {
62
+ if (stack.length === 0) {
63
+ throw new Error('Foldkit: clearRuntime called on an empty runtime stack. This means ' +
64
+ 'a `pushBoundary` or `setRuntime` was not paired with `clearRuntime` ' +
65
+ '(or vice versa) upstream. Likely a bug in a custom Submodel ' +
66
+ 'integration or view-time helper.');
67
+ }
68
+ stack.pop();
69
+ };
70
+ /** Returns the current dispatcher. For frames in the root boundary, this
71
+ * is the runtime's actual dispatch; for nested Submodel boundaries, this
72
+ * is a cached per-boundary dispatcher that applies the wrapping chain at
73
+ * event-fire time. Throws when called outside of a runtime frame. */
74
+ export const requireDispatch = () => {
75
+ const frame = stack[stack.length - 1];
76
+ if (frame === undefined) {
77
+ throw new Error('Foldkit: html element constructors must be called inside a runtime-driven render');
78
+ }
79
+ return getOrCreateBoundaryDispatch(frame.boundaryRegistry, frame.outerDispatch, frame.boundaryId);
80
+ };
81
+ /** Returns the current runtime Effect Context, used by Mount integrations
82
+ * that fork message-producing Effects against the live runtime services. */
83
+ export const requireRuntimeContext = () => {
84
+ const frame = stack[stack.length - 1];
85
+ if (frame === undefined) {
86
+ throw new Error('Foldkit: html element constructors must be called inside a runtime-driven render');
87
+ }
88
+ return frame.runtimeContext;
89
+ };
90
+ /** Returns the current frame's boundary registry and boundary id. Used by
91
+ * `h.submodel` to register wrapping descriptors and compute child
92
+ * boundary ids. Throws when called outside of a runtime frame. */
93
+ export const requireBoundary = () => {
94
+ const frame = stack[stack.length - 1];
95
+ if (frame === undefined) {
96
+ throw new Error('Foldkit: h.submodel must be called inside a runtime-driven render');
97
+ }
98
+ return { registry: frame.boundaryRegistry, boundaryId: frame.boundaryId };
99
+ };
100
+ /** Returns the full current frame, used by `h.submodel` to snapshot the
101
+ * parent's context before pushing a child boundary. The snapshot is
102
+ * captured into slot-callback closures so they can replay the parent's
103
+ * full frame via {@link pushFrame} when invoked, instead of inheriting
104
+ * from whatever happens to be on the stack at invocation time. Throws
105
+ * when called outside of a runtime frame. */
106
+ export const getCurrentFrame = () => {
107
+ const frame = stack[stack.length - 1];
108
+ if (frame === undefined) {
109
+ throw new Error('Foldkit: getCurrentFrame called without an active runtime frame');
110
+ }
111
+ return frame;
112
+ };
@@ -0,0 +1,98 @@
1
+ import type { VNode } from '../vdom.js';
2
+ declare const SUBMODEL_MESSAGE_BRAND = "__submodelMessage";
3
+ /** A view function branded with the Message type it dispatches. Build
4
+ * one with {@link defineView}:
5
+ *
6
+ * ```ts
7
+ * export const view = defineView<Counter.Model, Counter.Message>(
8
+ * (model) => h.button([h.OnClick(Increment())], ['+']),
9
+ * )
10
+ * ```
11
+ *
12
+ * When `ViewInputs` is provided, the view takes a second `viewInputs`
13
+ * argument:
14
+ *
15
+ * ```ts
16
+ * export const view = defineView<
17
+ * Checkbox.Model,
18
+ * Checkbox.Message,
19
+ * ViewInputs
20
+ * >((model, viewInputs) => viewInputs.toView({ checkbox: [...] }))
21
+ * ```
22
+ *
23
+ * Required at the `h.submodel` call site so unbranded plain functions
24
+ * fail to type-check there. */
25
+ export type SubmodelView<Model, Message, ViewInputs = void> = (ViewInputs extends void ? (model: Model) => VNode | null : (model: Model, viewInputs: ViewInputs) => VNode | null) & {
26
+ readonly [SUBMODEL_MESSAGE_BRAND]: Message;
27
+ };
28
+ /** Defines the view function of a Submodel, a child component embedded
29
+ * via `h.submodel`.
30
+ *
31
+ * Use this ONLY for views that will be embedded via `h.submodel`. Plain
32
+ * view functions (page-level render functions, helper render functions
33
+ * that compose Html, etc.) don't need to be defined this way. Write
34
+ * them as ordinary `(model) => Html` functions.
35
+ *
36
+ * Explicit type arguments are required because Message has no
37
+ * inferable source on the function signature itself. */
38
+ export declare const defineView: <Model, Message, ViewInputs = void>(fn: ViewInputs extends void ? (model: Model) => VNode | null : (model: Model, viewInputs: ViewInputs) => VNode | null) => SubmodelView<Model, Message, ViewInputs>;
39
+ type AnySubmodelView = ((...args: ReadonlyArray<any>) => VNode | null) & {
40
+ readonly [SUBMODEL_MESSAGE_BRAND]: unknown;
41
+ };
42
+ type ViewModelOf<View extends AnySubmodelView> = Parameters<View>[0];
43
+ type ViewInputsOf<View extends AnySubmodelView> = Parameters<View> extends [unknown, infer ViewInputs] ? ViewInputs : void;
44
+ type ViewMessageOf<View extends AnySubmodelView> = View extends {
45
+ readonly [SUBMODEL_MESSAGE_BRAND]: infer Message;
46
+ } ? Message : never;
47
+ /** Configuration for embedding a child Submodel into a parent's view.
48
+ *
49
+ * - `slotId`: unique identifier for this Submodel instance under the
50
+ * current boundary. Name the slot semantically (e.g.
51
+ * `'sidebar-group'`). For lists, use a stable per-item id (typically
52
+ * `entry.id`), not the array index. If the same model is rendered in
53
+ * two DOM positions (desktop + mobile, master + detail), each slot
54
+ * needs its own id (e.g. `'desktop-sidebar-group'`,
55
+ * `'mobile-sidebar-group'`). Two `h.submodel` calls inside the same
56
+ * parent boundary with the same `slotId` throw at view-build time,
57
+ * including across `createLazy`/`createKeyedLazy` cache hits.
58
+ * - `view`: the child's `SubmodelView`. Must be branded via
59
+ * {@link defineView} so `h.submodel` can infer the child's Message
60
+ * type. Unbranded plain functions fail to type-check here.
61
+ * - `model`: the child's model, inferred from `view`'s first parameter.
62
+ * Compared by `===` when the boundary is wrapped in a memoizing
63
+ * helper such as `createKeyedLazy`.
64
+ * - `viewInputs`: optional second-argument data passed to `view`,
65
+ * inferred from `view`'s second parameter. Function values AT THE TOP
66
+ * LEVEL of `viewInputs` (slot callbacks like `toView`) are
67
+ * auto-wrapped to execute in the parent's boundary so handlers the
68
+ * consumer builds inside them dispatch through the parent's wrapping
69
+ * chain. Function values nested below the top level (e.g.
70
+ * `viewInputs: { config: { onSubmit } }`) throw at view-build time
71
+ * with a path-based error like `viewInputs.config.onSubmit`. The
72
+ * check is runtime-only (TypeScript cannot distinguish a
73
+ * user-declared nested callback from a data value whose prototype
74
+ * carries methods), so a misuse compiles cleanly and surfaces the
75
+ * first time the boundary renders. Keep slot callbacks at the top
76
+ * level of `viewInputs`.
77
+ * - `toParentMessage`: function that lifts a child message into the
78
+ * current boundary's Message type. The argument is typed as the
79
+ * child's Message via the view's brand, so destructuring is correctly
80
+ * typed without annotation. For per-instance identifiers, capture
81
+ * them in a closure
82
+ * (`(message) => GotEntryMessage({ entryId: entry.id, message })`).
83
+ *
84
+ * High-level events the parent handles declaratively flow through
85
+ * each Submodel's `OutMessage`. The parent's `GotChildMessage`
86
+ * handler unpacks the third tuple element of the child's `update`
87
+ * return and pattern-matches on `Option<OutMessage>`. See `Ui.Menu`,
88
+ * `Ui.Listbox`, etc., for examples. */
89
+ export type SubmodelConfig<View extends AnySubmodelView> = Readonly<{
90
+ slotId: string;
91
+ model: ViewModelOf<View>;
92
+ view: View;
93
+ viewInputs?: ViewInputsOf<View>;
94
+ toParentMessage: (message: ViewMessageOf<View>) => unknown;
95
+ }>;
96
+ export declare const submodel: <View extends AnySubmodelView>(config: SubmodelConfig<View>) => VNode | null;
97
+ export {};
98
+ //# sourceMappingURL=submodel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submodel.d.ts","sourceRoot":"","sources":["../../src/html/submodel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAoBvC,QAAA,MAAM,sBAAsB,sBAAsB,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;;gCAqBgC;AAChC,MAAM,MAAM,YAAY,CACtB,KAAK,EACL,OAAO,EACP,UAAU,GAAG,IAAI,IACf,CAAC,UAAU,SAAS,IAAI,GACxB,CAAC,KAAK,EAAE,KAAK,KAAK,KAAK,GAAG,IAAI,GAC9B,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,KAAK,KAAK,GAAG,IAAI,CAAC,GAAG;IAC5D,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,OAAO,CAAA;CAC3C,CAAA;AAED;;;;;;;;;yDASyD;AACzD,eAAO,MAAM,UAAU,GAAI,KAAK,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,EAC1D,IAAI,UAAU,SAAS,IAAI,GACvB,CAAC,KAAK,EAAE,KAAK,KAAK,KAAK,GAAG,IAAI,GAC9B,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,KAAK,KAAK,GAAG,IAAI,KACzD,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,CAQM,CAAA;AAEhD,KAAK,eAAe,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,KAAK,KAAK,GAAG,IAAI,CAAC,GAAG;IACvE,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,OAAO,CAAA;CAC3C,CAAA;AAED,KAAK,WAAW,CAAC,IAAI,SAAS,eAAe,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;AAEpE,KAAK,YAAY,CAAC,IAAI,SAAS,eAAe,IAC5C,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,UAAU,CAAC,GAAG,UAAU,GAAG,IAAI,CAAA;AAE1E,KAAK,aAAa,CAAC,IAAI,SAAS,eAAe,IAAI,IAAI,SAAS;IAC9D,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAA;CACjD,GACG,OAAO,GACP,KAAK,CAAA;AAET;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wCAyCwC;AACxC,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,eAAe,IAAI,QAAQ,CAAC;IAClE,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IACxB,IAAI,EAAE,IAAI,CAAA;IACV,UAAU,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAA;IAC/B,eAAe,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,OAAO,CAAA;CAC3D,CAAC,CAAA;AA2IF,eAAO,MAAM,QAAQ,GAAI,IAAI,SAAS,eAAe,EACnD,QAAQ,cAAc,CAAC,IAAI,CAAC,KAC3B,KAAK,GAAG,IAsDV,CAAA"}
@@ -0,0 +1,190 @@
1
+ import { Predicate } from 'effect';
2
+ import { composeBoundary, deregisterBoundaryWrap, registerBoundaryWrap, } from './boundary.js';
3
+ import { isChildAttribute } from './childAttribute.js';
4
+ import { clearRuntime, getCurrentFrame, pushBoundary, pushFrame, } from './runtimeSingleton.js';
5
+ // NOTE: string key (not Symbol) so SubmodelView types from different
6
+ // module instances (e.g. pnpm hoisting variations) stay structurally
7
+ // compatible.
8
+ const SUBMODEL_MESSAGE_BRAND = '__submodelMessage';
9
+ /** Defines the view function of a Submodel, a child component embedded
10
+ * via `h.submodel`.
11
+ *
12
+ * Use this ONLY for views that will be embedded via `h.submodel`. Plain
13
+ * view functions (page-level render functions, helper render functions
14
+ * that compose Html, etc.) don't need to be defined this way. Write
15
+ * them as ordinary `(model) => Html` functions.
16
+ *
17
+ * Explicit type arguments are required because Message has no
18
+ * inferable source on the function signature itself. */
19
+ export const defineView = (fn) =>
20
+ // NOTE: The cast attaches the SUBMODEL_MESSAGE_BRAND to the runtime
21
+ // function value at the type level only. Message has no inferable
22
+ // source on the function signature itself, so the brand carries it.
23
+ // `h.submodel` reads the brand at the embed site to type-check
24
+ // `toParentMessage`. There is no runtime brand to add; the cast is
25
+ // the entire mechanism.
26
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
27
+ fn;
28
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
29
+ /** Walks below the top level of `viewInputs` and throws if it finds a
30
+ * function. Top-level functions are auto-scoped to the parent
31
+ * boundary; functions nested inside an object value or array element
32
+ * would silently capture the child's boundary and dispatch through
33
+ * the child's wrapping chain, which is almost certainly not what the
34
+ * consumer meant. Failing loud at view-build time is cheaper than a
35
+ * confused bug report from a misrouted Message. */
36
+ const assertNoNestedFunctions = (viewInputs) => {
37
+ for (const key of Object.keys(viewInputs)) {
38
+ const value = viewInputs[key];
39
+ if (isFrameworkBranded(value)) {
40
+ continue;
41
+ }
42
+ if (isPlainObject(value) || Array.isArray(value)) {
43
+ walkForFunctions(value, [key]);
44
+ }
45
+ }
46
+ };
47
+ // Framework-branded values that legitimately carry function members
48
+ // internally (e.g. `ChildAttribute.dispatch`). The walker treats these
49
+ // as opaque leaves, the same way it treats primitives.
50
+ const isFrameworkBranded = (value) => isChildAttribute(value);
51
+ const walkForFunctions = (source, path) => {
52
+ const visit = (value, segment) => {
53
+ const nextPath = [...path, segment];
54
+ if (typeof value === 'function') {
55
+ throw new Error(`Foldkit: h.submodel \`viewInputs\` may only contain functions at the ` +
56
+ `top level. Found a function at \`viewInputs.${nextPath.join('.')}\`. ` +
57
+ `Lift it to the top level of \`viewInputs\` so it can be auto-scoped to ` +
58
+ `the parent boundary, or pass the value as primitive data.`);
59
+ }
60
+ if (isFrameworkBranded(value)) {
61
+ return;
62
+ }
63
+ if (isPlainObject(value) || Array.isArray(value)) {
64
+ walkForFunctions(value, nextPath);
65
+ }
66
+ };
67
+ if (Array.isArray(source)) {
68
+ source.forEach((element, index) => visit(element, `[${index}]`));
69
+ }
70
+ else if (isPlainObject(source)) {
71
+ for (const key of Object.keys(source)) {
72
+ visit(source[key], key);
73
+ }
74
+ }
75
+ };
76
+ const wrapViewInputsForOuterBoundary = (viewInputs, outerFrame) => {
77
+ if (!isPlainObject(viewInputs)) {
78
+ return viewInputs;
79
+ }
80
+ assertNoNestedFunctions(viewInputs);
81
+ const wrapped = {};
82
+ for (const key of Object.keys(viewInputs)) {
83
+ const value = viewInputs[key];
84
+ if (typeof value === 'function') {
85
+ // Capture the parent's full frame (dispatch, context, registry,
86
+ // boundaryId) at wrap time. The slot callback uses `pushFrame` to
87
+ // replay that exact frame on every invocation, regardless of what
88
+ // happens to be on the stack at call time. Without this, a
89
+ // callback invoked from a deferred context (setTimeout, stored
90
+ // callback) would inherit from whatever render's frame was active,
91
+ // silently mis-binding dispatch and registry.
92
+ wrapped[key] = (...args) => {
93
+ pushFrame(outerFrame);
94
+ try {
95
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
96
+ return value(...args);
97
+ }
98
+ finally {
99
+ clearRuntime();
100
+ }
101
+ };
102
+ }
103
+ else {
104
+ wrapped[key] = value;
105
+ }
106
+ }
107
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
108
+ return wrapped;
109
+ };
110
+ /** Returns a copy of the vnode with a snabbdom `destroy` hook that
111
+ * deregisters this Submodel's boundary when the DOM node is removed.
112
+ * Composes with any existing destroy hook the user's view may have set.
113
+ *
114
+ * Copies the vnode (rather than mutating in place) so module-level
115
+ * cached vnodes a user might return from view are not contaminated with
116
+ * a destroy hook bound to this boundary id.
117
+ *
118
+ * This is what lets `h.submodel` survive cache hits from
119
+ * `createKeyedLazy`. When a cached vnode is reused across renders,
120
+ * snabbdom doesn't fire destroy, so the wrap stays registered and
121
+ * dispatches continue to route correctly. When the vnode is actually
122
+ * removed (entry deleted from a list, conditional render flips),
123
+ * destroy fires and the wrap is evicted: bounded memory, no leaks.
124
+ *
125
+ * See `submodel.test.ts` for the cache-hit-survival and
126
+ * destroy-deregisters-wrap assertions. */
127
+ const withBoundaryCleanup = (vnode, registry, boundaryId) => {
128
+ const data = vnode.data ?? {};
129
+ const hook = data.hook ?? {};
130
+ const previousDestroy = hook.destroy;
131
+ const compositeDestroy = (removed) => {
132
+ deregisterBoundaryWrap(registry, boundaryId);
133
+ if (previousDestroy !== undefined) {
134
+ previousDestroy(removed);
135
+ }
136
+ };
137
+ return {
138
+ ...vnode,
139
+ data: { ...data, hook: { ...hook, destroy: compositeDestroy } },
140
+ };
141
+ };
142
+ export const submodel = (config) => {
143
+ // Snapshot the parent frame BEFORE pushing the child boundary. The
144
+ // snapshot is captured into slot-callback closures by
145
+ // `wrapViewInputsForOuterBoundary` so they can replay the parent's
146
+ // full frame when invoked.
147
+ const parentFrame = getCurrentFrame();
148
+ const registry = parentFrame.boundaryRegistry;
149
+ const childBoundaryId = composeBoundary(parentFrame.boundaryId, config.slotId);
150
+ registerBoundaryWrap(registry, childBoundaryId, {
151
+ toParentMessage:
152
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
153
+ config.toParentMessage,
154
+ });
155
+ let vnode;
156
+ pushBoundary(childBoundaryId);
157
+ try {
158
+ try {
159
+ if (Predicate.isUndefined(config.viewInputs)) {
160
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
161
+ const view = config.view;
162
+ vnode = view(config.model);
163
+ }
164
+ else {
165
+ const wrappedViewInputs = wrapViewInputsForOuterBoundary(
166
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
167
+ config.viewInputs, parentFrame);
168
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
169
+ const view = config.view;
170
+ vnode = view(config.model, wrappedViewInputs);
171
+ }
172
+ }
173
+ catch (error) {
174
+ // The view threw; the registered wrap would otherwise leak with
175
+ // no destroy hook ever firing. Drop it before propagating.
176
+ deregisterBoundaryWrap(registry, childBoundaryId);
177
+ throw error;
178
+ }
179
+ }
180
+ finally {
181
+ clearRuntime();
182
+ }
183
+ if (vnode === null) {
184
+ // No vnode means no destroy hook will ever fire; deregister now so
185
+ // the wrap doesn't leak.
186
+ deregisterBoundaryWrap(registry, childBoundaryId);
187
+ return null;
188
+ }
189
+ return withBoundaryCleanup(vnode, registry, childBoundaryId);
190
+ };
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * as Route from './route/public.js';
15
15
  export * as Runtime from './runtime/public.js';
16
16
  export * as Schema from './schema/public.js';
17
17
  export * as Struct from './struct/public.js';
18
+ export * as Submodel from './submodel/public.js';
18
19
  export * as Subscription from './subscription/public.js';
19
20
  export * as Scene from './test/scene.js';
20
21
  export * as Story from './test/story.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAA;AAChD,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,aAAa,MAAM,2BAA2B,CAAA;AAC1D,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAA;AACtC,OAAO,KAAK,eAAe,MAAM,6BAA6B,CAAA;AAC9D,OAAO,KAAK,eAAe,MAAM,6BAA6B,CAAA;AAC9D,OAAO,KAAK,IAAI,MAAM,kBAAkB,CAAA;AACxC,OAAO,KAAK,IAAI,MAAM,kBAAkB,CAAA;AACxC,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,KAAK,MAAM,mBAAmB,CAAA;AAC1C,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAA;AACpD,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,KAAK,MAAM,mBAAmB,CAAA;AAC1C,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,YAAY,MAAM,0BAA0B,CAAA;AACxD,OAAO,KAAK,KAAK,MAAM,iBAAiB,CAAA;AACxC,OAAO,KAAK,KAAK,MAAM,iBAAiB,CAAA;AACxC,OAAO,KAAK,EAAE,MAAM,eAAe,CAAA;AACnC,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAA;AAChD,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,aAAa,MAAM,2BAA2B,CAAA;AAC1D,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAA;AACtC,OAAO,KAAK,eAAe,MAAM,6BAA6B,CAAA;AAC9D,OAAO,KAAK,eAAe,MAAM,6BAA6B,CAAA;AAC9D,OAAO,KAAK,IAAI,MAAM,kBAAkB,CAAA;AACxC,OAAO,KAAK,IAAI,MAAM,kBAAkB,CAAA;AACxC,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,KAAK,MAAM,mBAAmB,CAAA;AAC1C,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAA;AACpD,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,KAAK,MAAM,mBAAmB,CAAA;AAC1C,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAA;AAC9C,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAA;AAChD,OAAO,KAAK,YAAY,MAAM,0BAA0B,CAAA;AACxD,OAAO,KAAK,KAAK,MAAM,iBAAiB,CAAA;AACxC,OAAO,KAAK,KAAK,MAAM,iBAAiB,CAAA;AACxC,OAAO,KAAK,EAAE,MAAM,eAAe,CAAA;AACnC,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAA"}
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * as Route from './route/public.js';
15
15
  export * as Runtime from './runtime/public.js';
16
16
  export * as Schema from './schema/public.js';
17
17
  export * as Struct from './struct/public.js';
18
+ export * as Submodel from './submodel/public.js';
18
19
  export * as Subscription from './subscription/public.js';
19
20
  export * as Scene from './test/scene.js';
20
21
  export * as Story from './test/story.js';
@@ -33,7 +33,7 @@ export declare const afterCommit: Effect.Effect<void>;
33
33
  *
34
34
  * @example
35
35
  * ```typescript
36
- * Render.afterPaint.pipe(Effect.as(TransitionFrameAdvanced()))
36
+ * Render.afterPaint
37
37
  * ```
38
38
  */
39
39
  export declare const afterPaint: Effect.Effect<void>;