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.
- package/README.md +2 -1
- package/dist/canvas/view.d.ts +1 -1
- package/dist/canvas/view.d.ts.map +1 -1
- package/dist/canvas/view.js +5 -5
- package/dist/command/index.d.ts +71 -0
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +34 -1
- package/dist/command/public.d.ts +1 -1
- package/dist/command/public.d.ts.map +1 -1
- package/dist/command/public.js +1 -1
- package/dist/devTools/overlay.d.ts.map +1 -1
- package/dist/devTools/overlay.js +137 -110
- package/dist/dom/dom.d.ts +8 -11
- package/dist/dom/dom.d.ts.map +1 -1
- package/dist/dom/dom.js +8 -11
- package/dist/dom/elementMovement.d.ts +1 -3
- package/dist/dom/elementMovement.d.ts.map +1 -1
- package/dist/dom/elementMovement.js +1 -3
- package/dist/dom/inert.d.ts +2 -4
- package/dist/dom/inert.d.ts.map +1 -1
- package/dist/dom/inert.js +2 -4
- package/dist/dom/scrollLock.d.ts +2 -2
- package/dist/dom/scrollLock.js +2 -2
- package/dist/dom/waitForAnimation.d.ts +1 -1
- package/dist/dom/waitForAnimation.js +1 -1
- package/dist/html/boundary.d.ts +98 -0
- package/dist/html/boundary.d.ts.map +1 -0
- package/dist/html/boundary.js +176 -0
- package/dist/html/childAttribute.d.ts +44 -0
- package/dist/html/childAttribute.d.ts.map +1 -0
- package/dist/html/childAttribute.js +34 -0
- package/dist/html/index.d.ts +70 -23
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +639 -575
- package/dist/html/lazy.d.ts +12 -7
- package/dist/html/lazy.d.ts.map +1 -1
- package/dist/html/lazy.js +30 -11
- package/dist/html/public.d.ts +2 -2
- package/dist/html/public.d.ts.map +1 -1
- package/dist/html/public.js +1 -1
- package/dist/html/runtimeSingleton.d.ts +72 -0
- package/dist/html/runtimeSingleton.d.ts.map +1 -0
- package/dist/html/runtimeSingleton.js +112 -0
- package/dist/html/submodel.d.ts +98 -0
- package/dist/html/submodel.d.ts.map +1 -0
- package/dist/html/submodel.js +190 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/render/render.d.ts +1 -1
- package/dist/render/render.js +1 -1
- package/dist/runtime/messagePriority.d.ts +5 -1
- package/dist/runtime/messagePriority.d.ts.map +1 -1
- package/dist/runtime/messagePriority.js +25 -4
- package/dist/runtime/runtime.d.ts +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +115 -61
- package/dist/submodel/public.d.ts +4 -0
- package/dist/submodel/public.d.ts.map +1 -0
- package/dist/submodel/public.js +1 -0
- package/dist/submodel/submodel.d.ts +32 -0
- package/dist/submodel/submodel.d.ts.map +1 -0
- package/dist/submodel/submodel.js +1 -0
- package/dist/test/apps/disabledButton.d.ts +4 -5
- package/dist/test/apps/disabledButton.d.ts.map +1 -1
- package/dist/test/apps/disabledButton.js +16 -16
- package/dist/test/scene.d.ts +8 -8
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +25 -13
- package/dist/test/story.d.ts +15 -8
- package/dist/test/story.d.ts.map +1 -1
- package/dist/test/story.js +21 -9
- package/dist/ui/animation/index.d.ts +30 -14
- package/dist/ui/animation/index.d.ts.map +1 -1
- package/dist/ui/animation/index.js +9 -19
- package/dist/ui/animation/public.d.ts +2 -2
- package/dist/ui/animation/public.d.ts.map +1 -1
- package/dist/ui/animation/public.js +1 -1
- package/dist/ui/calendar/index.d.ts +199 -84
- package/dist/ui/calendar/index.d.ts.map +1 -1
- package/dist/ui/calendar/index.js +129 -140
- package/dist/ui/calendar/public.d.ts +2 -2
- package/dist/ui/calendar/public.d.ts.map +1 -1
- package/dist/ui/calendar/public.js +1 -1
- package/dist/ui/checkbox/index.d.ts +93 -21
- package/dist/ui/checkbox/index.d.ts.map +1 -1
- package/dist/ui/checkbox/index.js +62 -33
- package/dist/ui/checkbox/public.d.ts +2 -2
- package/dist/ui/checkbox/public.d.ts.map +1 -1
- package/dist/ui/checkbox/public.js +1 -1
- package/dist/ui/combobox/multi.d.ts +35 -91
- package/dist/ui/combobox/multi.d.ts.map +1 -1
- package/dist/ui/combobox/multi.js +34 -17
- package/dist/ui/combobox/multiPublic.d.ts +2 -2
- package/dist/ui/combobox/multiPublic.d.ts.map +1 -1
- package/dist/ui/combobox/multiPublic.js +1 -1
- package/dist/ui/combobox/public.d.ts +3 -3
- package/dist/ui/combobox/public.d.ts.map +1 -1
- package/dist/ui/combobox/public.js +2 -2
- package/dist/ui/combobox/shared.d.ts +56 -31
- package/dist/ui/combobox/shared.d.ts.map +1 -1
- package/dist/ui/combobox/shared.js +333 -322
- package/dist/ui/combobox/single.d.ts +46 -93
- package/dist/ui/combobox/single.d.ts.map +1 -1
- package/dist/ui/combobox/single.js +44 -17
- package/dist/ui/datePicker/index.d.ts +256 -48
- package/dist/ui/datePicker/index.d.ts.map +1 -1
- package/dist/ui/datePicker/index.js +149 -104
- package/dist/ui/datePicker/public.d.ts +2 -2
- package/dist/ui/datePicker/public.d.ts.map +1 -1
- package/dist/ui/datePicker/public.js +1 -1
- package/dist/ui/dialog/index.d.ts +95 -39
- package/dist/ui/dialog/index.d.ts.map +1 -1
- package/dist/ui/dialog/index.js +71 -62
- package/dist/ui/dialog/public.d.ts +2 -2
- package/dist/ui/dialog/public.d.ts.map +1 -1
- package/dist/ui/dialog/public.js +1 -1
- package/dist/ui/disclosure/index.d.ts +71 -31
- package/dist/ui/disclosure/index.d.ts.map +1 -1
- package/dist/ui/disclosure/index.js +57 -62
- package/dist/ui/disclosure/public.d.ts +2 -2
- package/dist/ui/disclosure/public.d.ts.map +1 -1
- package/dist/ui/disclosure/public.js +1 -1
- package/dist/ui/dragAndDrop/index.d.ts +6 -6
- package/dist/ui/dragAndDrop/index.d.ts.map +1 -1
- package/dist/ui/dragAndDrop/index.js +7 -7
- package/dist/ui/dragAndDrop/public.d.ts +1 -1
- package/dist/ui/dragAndDrop/public.d.ts.map +1 -1
- package/dist/ui/dragAndDrop/public.js +1 -1
- package/dist/ui/fileDrop/index.d.ts +42 -46
- package/dist/ui/fileDrop/index.d.ts.map +1 -1
- package/dist/ui/fileDrop/index.js +30 -46
- package/dist/ui/fileDrop/public.d.ts +2 -2
- package/dist/ui/fileDrop/public.d.ts.map +1 -1
- package/dist/ui/fileDrop/public.js +1 -1
- package/dist/ui/listbox/multi.d.ts +39 -84
- package/dist/ui/listbox/multi.d.ts.map +1 -1
- package/dist/ui/listbox/multi.js +38 -20
- package/dist/ui/listbox/multiPublic.d.ts +2 -2
- package/dist/ui/listbox/multiPublic.d.ts.map +1 -1
- package/dist/ui/listbox/multiPublic.js +1 -1
- package/dist/ui/listbox/public.d.ts +3 -3
- package/dist/ui/listbox/public.d.ts.map +1 -1
- package/dist/ui/listbox/public.js +2 -2
- package/dist/ui/listbox/shared.d.ts +71 -30
- package/dist/ui/listbox/shared.d.ts.map +1 -1
- package/dist/ui/listbox/shared.js +319 -296
- package/dist/ui/listbox/single.d.ts +57 -85
- package/dist/ui/listbox/single.d.ts.map +1 -1
- package/dist/ui/listbox/single.js +48 -24
- package/dist/ui/menu/index.d.ts +80 -36
- package/dist/ui/menu/index.d.ts.map +1 -1
- package/dist/ui/menu/index.js +117 -86
- package/dist/ui/menu/public.d.ts +2 -2
- package/dist/ui/menu/public.d.ts.map +1 -1
- package/dist/ui/menu/public.js +1 -1
- package/dist/ui/popover/index.d.ts +117 -44
- package/dist/ui/popover/index.d.ts.map +1 -1
- package/dist/ui/popover/index.js +88 -101
- package/dist/ui/popover/public.d.ts +2 -2
- package/dist/ui/popover/public.d.ts.map +1 -1
- package/dist/ui/popover/public.js +1 -1
- package/dist/ui/radioGroup/index.d.ts +122 -45
- package/dist/ui/radioGroup/index.d.ts.map +1 -1
- package/dist/ui/radioGroup/index.js +111 -72
- package/dist/ui/radioGroup/public.d.ts +2 -2
- package/dist/ui/radioGroup/public.d.ts.map +1 -1
- package/dist/ui/radioGroup/public.js +1 -1
- package/dist/ui/slider/index.d.ts +72 -34
- package/dist/ui/slider/index.d.ts.map +1 -1
- package/dist/ui/slider/index.js +40 -49
- package/dist/ui/slider/public.d.ts +2 -2
- package/dist/ui/slider/public.d.ts.map +1 -1
- package/dist/ui/slider/public.js +1 -1
- package/dist/ui/switch/index.d.ts +74 -21
- package/dist/ui/switch/index.d.ts.map +1 -1
- package/dist/ui/switch/index.js +62 -33
- package/dist/ui/switch/public.d.ts +2 -2
- package/dist/ui/switch/public.d.ts.map +1 -1
- package/dist/ui/switch/public.js +1 -1
- package/dist/ui/tabs/index.d.ts +107 -45
- package/dist/ui/tabs/index.d.ts.map +1 -1
- package/dist/ui/tabs/index.js +99 -81
- package/dist/ui/tabs/public.d.ts +2 -2
- package/dist/ui/tabs/public.d.ts.map +1 -1
- package/dist/ui/tabs/public.js +1 -1
- package/dist/ui/toast/index.d.ts +93 -109
- package/dist/ui/toast/index.d.ts.map +1 -1
- package/dist/ui/toast/index.js +16 -29
- package/dist/ui/toast/schema.d.ts +15 -4
- package/dist/ui/toast/schema.d.ts.map +1 -1
- package/dist/ui/toast/schema.js +11 -4
- package/dist/ui/toast/update.d.ts +36 -18
- package/dist/ui/toast/update.d.ts.map +1 -1
- package/dist/ui/toast/update.js +33 -14
- package/dist/ui/tooltip/index.d.ts +94 -42
- package/dist/ui/tooltip/index.d.ts.map +1 -1
- package/dist/ui/tooltip/index.js +64 -73
- package/dist/ui/tooltip/public.d.ts +2 -2
- package/dist/ui/tooltip/public.d.ts.map +1 -1
- package/dist/ui/tooltip/public.js +1 -1
- package/dist/ui/virtualList/index.d.ts +18 -41
- package/dist/ui/virtualList/index.d.ts.map +1 -1
- package/dist/ui/virtualList/index.js +17 -37
- package/dist/ui/virtualList/public.d.ts +2 -2
- package/dist/ui/virtualList/public.d.ts.map +1 -1
- package/dist/ui/virtualList/public.js +1 -1
- package/package.json +1 -1
package/dist/html/lazy.d.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
4
|
-
*
|
|
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) =>
|
|
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
|
|
19
|
-
*
|
|
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) =>
|
|
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
|
package/dist/html/lazy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy.d.ts","sourceRoot":"","sources":["../../src/html/lazy.ts"],"names":[],"mappings":"
|
|
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 {
|
|
2
|
-
import {
|
|
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) =>
|
|
6
|
-
const 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
|
|
14
|
-
|
|
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
|
|
19
|
-
*
|
|
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
|
|
39
|
-
*
|
|
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
|
package/dist/html/public.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/html/public.js
CHANGED
|
@@ -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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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';
|
package/dist/render/render.d.ts
CHANGED