foldkit 0.157.0 → 0.158.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/dist/devTools/host.d.ts +2 -2
- package/dist/devTools/host.d.ts.map +1 -1
- package/dist/devTools/host.js +1 -1
- package/dist/experimental/machine/machine.d.ts +278 -84
- package/dist/experimental/machine/machine.d.ts.map +1 -1
- package/dist/experimental/machine/machine.js +362 -212
- package/dist/runtime/browserListeners.d.ts +7 -1
- package/dist/runtime/browserListeners.d.ts.map +1 -1
- package/dist/runtime/browserScheduler.d.ts +3 -0
- package/dist/runtime/browserScheduler.d.ts.map +1 -0
- package/dist/runtime/browserScheduler.js +22 -0
- package/dist/runtime/crashUI.d.ts +36 -5
- package/dist/runtime/crashUI.d.ts.map +1 -1
- package/dist/runtime/crashUI.js +55 -2
- package/dist/runtime/devToolsConfig.d.ts +85 -0
- package/dist/runtime/devToolsConfig.d.ts.map +1 -0
- package/dist/runtime/devToolsConfig.js +49 -0
- package/dist/runtime/devToolsIntegration.d.ts +56 -0
- package/dist/runtime/devToolsIntegration.d.ts.map +1 -0
- package/dist/runtime/devToolsIntegration.js +186 -0
- package/dist/runtime/dispatch.d.ts +10 -0
- package/dist/runtime/dispatch.d.ts.map +1 -0
- package/dist/runtime/dispatch.js +4 -0
- package/dist/runtime/documentMetadata.d.ts +3 -0
- package/dist/runtime/documentMetadata.d.ts.map +1 -0
- package/dist/runtime/documentMetadata.js +148 -0
- package/dist/runtime/duplicateIdScanner.d.ts +16 -0
- package/dist/runtime/duplicateIdScanner.d.ts.map +1 -0
- package/dist/runtime/duplicateIdScanner.js +70 -0
- package/dist/runtime/hmrModelBridge.d.ts +4 -0
- package/dist/runtime/hmrModelBridge.d.ts.map +1 -0
- package/dist/runtime/hmrModelBridge.js +43 -0
- package/dist/runtime/hostConnector.d.ts +61 -0
- package/dist/runtime/hostConnector.d.ts.map +1 -0
- package/dist/runtime/hostConnector.js +141 -0
- package/dist/runtime/hydrationHandoff.d.ts +44 -0
- package/dist/runtime/hydrationHandoff.d.ts.map +1 -0
- package/dist/runtime/hydrationHandoff.js +395 -0
- package/dist/runtime/index.d.ts +16 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +5 -1
- package/dist/runtime/makeApplication.d.ts +71 -0
- package/dist/runtime/makeApplication.d.ts.map +1 -0
- package/dist/runtime/makeApplication.js +98 -0
- package/dist/runtime/makeElement.d.ts +69 -0
- package/dist/runtime/makeElement.d.ts.map +1 -0
- package/dist/runtime/makeElement.js +81 -0
- package/dist/runtime/managedResourceFibers.d.ts +18 -0
- package/dist/runtime/managedResourceFibers.d.ts.map +1 -0
- package/dist/runtime/managedResourceFibers.js +47 -0
- package/dist/runtime/messageQueue.d.ts +27 -0
- package/dist/runtime/messageQueue.d.ts.map +1 -0
- package/dist/runtime/messageQueue.js +133 -0
- package/dist/runtime/public.d.ts +2 -3
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/renderer.d.ts +69 -0
- package/dist/runtime/renderer.d.ts.map +1 -0
- package/dist/runtime/renderer.js +512 -0
- package/dist/runtime/resourceProvider.d.ts +33 -0
- package/dist/runtime/resourceProvider.d.ts.map +1 -0
- package/dist/runtime/resourceProvider.js +98 -0
- package/dist/runtime/runtime.d.ts +133 -412
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +118 -2184
- package/dist/runtime/runtimeStatus.d.ts +15 -0
- package/dist/runtime/runtimeStatus.d.ts.map +1 -0
- package/dist/runtime/runtimeStatus.js +6 -0
- package/dist/runtime/slowPhase.d.ts +82 -0
- package/dist/runtime/slowPhase.d.ts.map +1 -0
- package/dist/runtime/slowPhase.js +83 -0
- package/dist/runtime/start.d.ts +96 -0
- package/dist/runtime/start.d.ts.map +1 -0
- package/dist/runtime/start.js +99 -0
- package/dist/runtime/subscriptionFibers.d.ts +21 -0
- package/dist/runtime/subscriptionFibers.d.ts.map +1 -0
- package/dist/runtime/subscriptionFibers.js +47 -0
- package/dist/runtime/visibility.d.ts +8 -0
- package/dist/runtime/visibility.d.ts.map +1 -0
- package/dist/runtime/visibility.js +6 -0
- package/dist/test/scene.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserListeners.d.ts","sourceRoot":"","sources":["../../src/runtime/browserListeners.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browserListeners.d.ts","sourceRoot":"","sources":["../../src/runtime/browserListeners.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AACxD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAErC,oFAAoF;AACpF,MAAM,MAAM,aAAa,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC5C,YAAY,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAA;IAC9C,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAA;CACnC,CAAC,CAAA;AAEF,eAAO,MAAM,2BAA2B,GAAI,OAAO,YACvC,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,iBACrB,aAAa,CAAC,OAAO,CAAC,KACpC,CAAC,MAAM,IAAI,CAWb,CAAA;AAgBD,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAChC,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,iBACrB,aAAa,CAAC,OAAO,CAAC,KACpC,CAAC,MAAM,IAAI,CAwDb,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserScheduler.d.ts","sourceRoot":"","sources":["../../src/runtime/browserScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAA;AA0BjD,eAAO,MAAM,uBAAuB,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,UACrC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC7B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CACsD,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Effect, Layer, Scheduler } from 'effect';
|
|
2
|
+
// NOTE: scheduling fix for browser performance. Effect needs to defer work
|
|
3
|
+
// onto a future tick of the event loop. The default browser scheduler picks
|
|
4
|
+
// `setTimeout(f, 0)`, but browsers clamp `setTimeout` to a minimum of 4ms.
|
|
5
|
+
// `queueMicrotask` runs on the very next tick (sub-millisecond). Dispatch no
|
|
6
|
+
// longer routes through the Effect scheduler, but Command and Subscription
|
|
7
|
+
// fibers still do; without this override every fiber yield (for example, an
|
|
8
|
+
// op-budget suspension, or a Stream step) would take an extra 4-16ms
|
|
9
|
+
// round-trip before
|
|
10
|
+
// its result Message lands.
|
|
11
|
+
const microtaskSetImmediate = (callback) => {
|
|
12
|
+
let cancelled = false;
|
|
13
|
+
queueMicrotask(() => {
|
|
14
|
+
if (!cancelled)
|
|
15
|
+
callback();
|
|
16
|
+
});
|
|
17
|
+
return () => {
|
|
18
|
+
cancelled = true;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const browserScheduler = new Scheduler.MixedScheduler('async', microtaskSetImmediate);
|
|
22
|
+
export const provideBrowserScheduler = (effect) => Effect.provide(effect, Layer.succeed(Scheduler.Scheduler, browserScheduler));
|
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
import { Effect } from 'effect';
|
|
2
|
-
import { Document } from '../html/index.js';
|
|
1
|
+
import { Effect, Option } from 'effect';
|
|
2
|
+
import { Document, Html, type HtmlBuilder } from '../html/index.js';
|
|
3
|
+
import { VNode } from '../vdom.js';
|
|
4
|
+
/** Context provided to crash.view and crash.report when the runtime encounters
|
|
5
|
+
* an unrecoverable error. `message` is the Message being processed when the
|
|
6
|
+
* crash occurred, present as an `Option` because a crash during the initial
|
|
7
|
+
* render has no triggering Message. */
|
|
8
|
+
export type CrashContext<Model, Message> = Readonly<{
|
|
9
|
+
error: Error;
|
|
10
|
+
model: Model;
|
|
11
|
+
message: Option.Option<Message>;
|
|
12
|
+
}>;
|
|
13
|
+
/** Configuration for crash handling, with custom crash UI and/or crash
|
|
14
|
+
* reporting. The crash view renders after the dispatch loop has stopped, so
|
|
15
|
+
* its builder's Message is `never` and no handler is expressible. Reload or
|
|
16
|
+
* navigate with a raw DOM attribute instead. */
|
|
17
|
+
export type CrashConfig<Model, Message> = Readonly<{
|
|
18
|
+
view?: (context: CrashContext<Model, Message>, h: HtmlBuilder<never>) => Document;
|
|
19
|
+
report?: (context: CrashContext<Model, Message>) => void;
|
|
20
|
+
}>;
|
|
21
|
+
/** Configuration for crash handling in a `makeElement` app. The crash view
|
|
22
|
+
* returns `Html`, not a `Document`, because a scoped app never owns the
|
|
23
|
+
* document `<head>`. */
|
|
24
|
+
export type ElementCrashConfig<Model, Message> = Readonly<{
|
|
25
|
+
view?: (context: CrashContext<Model, Message>, h: HtmlBuilder<never>) => Html;
|
|
26
|
+
report?: (context: CrashContext<Model, Message>) => void;
|
|
27
|
+
}>;
|
|
3
28
|
export declare const noOpDispatch: {
|
|
4
29
|
dispatchAsync: (_message: unknown) => Effect.Effect<void, never, never>;
|
|
5
30
|
dispatchSync: (_message: unknown) => void;
|
|
6
31
|
};
|
|
7
|
-
export declare const defaultCrashView: (context:
|
|
8
|
-
|
|
9
|
-
|
|
32
|
+
export declare const defaultCrashView: (context: CrashContext<unknown, unknown>, viewError?: unknown) => Document;
|
|
33
|
+
/** Mutable holder for the vnode tree currently mounted in the container.
|
|
34
|
+
* The render frame writes it after every patch; the dispose finalizer, the
|
|
35
|
+
* replay render, and {@link renderCrashView} read it. A plain object rather
|
|
36
|
+
* than a Ref because every reader runs synchronously on the main thread. */
|
|
37
|
+
export type VNodeSlot = {
|
|
38
|
+
maybeCurrentVNode: Option.Option<VNode>;
|
|
39
|
+
};
|
|
40
|
+
export declare const renderCrashView: <Model, Message>(context: CrashContext<Model, Message>, crash: CrashConfig<Model, Message> | undefined, container: HTMLElement, vnodeSlot: VNodeSlot, manageDocument: boolean) => void;
|
|
10
41
|
//# sourceMappingURL=crashUI.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crashUI.d.ts","sourceRoot":"","sources":["../../src/runtime/crashUI.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"crashUI.d.ts","sourceRoot":"","sources":["../../src/runtime/crashUI.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,KAAK,WAAW,EAKjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,KAAK,EAAgB,MAAM,YAAY,CAAA;AAIhD;;;wCAGwC;AACxC,MAAM,MAAM,YAAY,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IAClD,KAAK,EAAE,KAAK,CAAA;IACZ,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CAChC,CAAC,CAAA;AAEF;;;iDAGiD;AACjD,MAAM,MAAM,WAAW,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACjD,IAAI,CAAC,EAAE,CACL,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,EACrC,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,KAClB,QAAQ,CAAA;IACb,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CACzD,CAAC,CAAA;AAEF;;yBAEyB;AACzB,MAAM,MAAM,kBAAkB,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACxD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,KAAK,IAAI,CAAA;IAC7E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CACzD,CAAC,CAAA;AAEF,eAAO,MAAM,YAAY;IACvB,aAAa,aAAa,OAAO;IACjC,YAAY,aAAa,OAAO;CACjC,CAAA;AAmBD,eAAO,MAAM,gBAAgB,YAClB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,cAC3B,OAAO,KAClB,QAwLF,CAAA;AAED;;;6EAG6E;AAC7E,MAAM,MAAM,SAAS,GAAG;IACtB,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;CACxC,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,KAAK,EAAE,OAAO,WACnC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,SAC9B,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,SAAS,aACnC,WAAW,aACX,SAAS,kBACJ,OAAO,KACtB,IA8DF,CAAA"}
|
package/dist/runtime/crashUI.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { Effect } from 'effect';
|
|
2
|
-
import { inertHtml as ih } from '../html/index.js';
|
|
1
|
+
import { Context, Effect, Option } from 'effect';
|
|
2
|
+
import { __clearRuntime as clearHtmlRuntime, __htmlBuilder as htmlBuilderFor, inertHtml as ih, __setRuntime as setHtmlRuntime, } from '../html/index.js';
|
|
3
|
+
import { MountTracker } from '../mount/index.js';
|
|
4
|
+
import { __patchVNode } from '../vdom.js';
|
|
5
|
+
import { Dispatch } from './dispatch.js';
|
|
6
|
+
import { applyDocumentMetadata } from './documentMetadata.js';
|
|
3
7
|
export const noOpDispatch = {
|
|
4
8
|
dispatchAsync: (_message) => Effect.void,
|
|
5
9
|
dispatchSync: (_message) => { },
|
|
@@ -163,3 +167,52 @@ export const defaultCrashView = (context, viewError) => {
|
|
|
163
167
|
]);
|
|
164
168
|
return { title: 'Application Crash', body };
|
|
165
169
|
};
|
|
170
|
+
export const renderCrashView = (context, crash, container, vnodeSlot, manageDocument) => {
|
|
171
|
+
console.error('[foldkit] Application crash:', context.error);
|
|
172
|
+
if (crash?.report) {
|
|
173
|
+
try {
|
|
174
|
+
crash.report(context);
|
|
175
|
+
}
|
|
176
|
+
catch (reportError) {
|
|
177
|
+
console.error('[foldkit] crash.report failed:', reportError);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const crashContext = Context.make(Dispatch, noOpDispatch).pipe(Context.add(MountTracker, {
|
|
181
|
+
started: () => { },
|
|
182
|
+
ended: () => { },
|
|
183
|
+
}));
|
|
184
|
+
try {
|
|
185
|
+
setHtmlRuntime(noOpDispatch.dispatchSync, crashContext);
|
|
186
|
+
let crashDocument;
|
|
187
|
+
try {
|
|
188
|
+
crashDocument = crash?.view
|
|
189
|
+
? crash.view(context, htmlBuilderFor())
|
|
190
|
+
: defaultCrashView(context);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
clearHtmlRuntime();
|
|
194
|
+
}
|
|
195
|
+
const patchedVNode = __patchVNode(vnodeSlot.maybeCurrentVNode, crashDocument.body, container);
|
|
196
|
+
vnodeSlot.maybeCurrentVNode = Option.some(patchedVNode);
|
|
197
|
+
if (manageDocument) {
|
|
198
|
+
applyDocumentMetadata(crashDocument, patchedVNode.elm);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (viewError) {
|
|
202
|
+
console.error('[foldkit] crash.view failed:', viewError);
|
|
203
|
+
const fallbackViewError = viewError instanceof Error ? viewError : new Error(String(viewError));
|
|
204
|
+
setHtmlRuntime(noOpDispatch.dispatchSync, crashContext);
|
|
205
|
+
let fallbackDocument;
|
|
206
|
+
try {
|
|
207
|
+
fallbackDocument = defaultCrashView(context, fallbackViewError);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
clearHtmlRuntime();
|
|
211
|
+
}
|
|
212
|
+
const patchedVNode = __patchVNode(vnodeSlot.maybeCurrentVNode, fallbackDocument.body, container);
|
|
213
|
+
vnodeSlot.maybeCurrentVNode = Option.some(patchedVNode);
|
|
214
|
+
if (manageDocument) {
|
|
215
|
+
applyDocumentMetadata(fallbackDocument, patchedVNode.elm);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Effect, Option, Schema, Scope } from 'effect';
|
|
2
|
+
import type { DevToolsStore } from '../devTools/store.js';
|
|
3
|
+
import { type Visibility } from './visibility.js';
|
|
4
|
+
/** Position of the DevTools badge and panel on screen. */
|
|
5
|
+
export type DevToolsPosition = 'BottomRight' | 'BottomLeft' | 'TopRight' | 'TopLeft';
|
|
6
|
+
/** Controls DevTools interaction mode.
|
|
7
|
+
*
|
|
8
|
+
* - `'Inspect'`: Messages stream in and clicking a row shows its state snapshot without pausing the rendered view.
|
|
9
|
+
* - `'TimeTravel'`: Clicking a row installs a paused historical view while the live application continues. Resume to patch the latest live view.
|
|
10
|
+
*/
|
|
11
|
+
export type DevToolsMode = 'Inspect' | 'TimeTravel';
|
|
12
|
+
/** Mode value for the DevTools panel. Either a single mode used in every
|
|
13
|
+
* environment, or an object selecting different modes for development and
|
|
14
|
+
* production. Use the object form to keep `'TimeTravel'` for local debugging
|
|
15
|
+
* while shipping the safer `'Inspect'` mode to users. `'TimeTravel'` in
|
|
16
|
+
* production pauses the user's rendered view when a history row is clicked. */
|
|
17
|
+
export type DevToolsModeConfig = DevToolsMode | Readonly<{
|
|
18
|
+
development: DevToolsMode;
|
|
19
|
+
production: DevToolsMode;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Factory that mounts the in-browser DevTools overlay against a recording
|
|
23
|
+
* store. The runtime keeps the store and the WebSocket bridge (so external
|
|
24
|
+
* tooling like the DevTools MCP server works without an overlay); the visual
|
|
25
|
+
* overlay is injected so it can live in `@foldkit/devtools` and pull in
|
|
26
|
+
* `@foldkit/ui` without coupling the core runtime to either.
|
|
27
|
+
*
|
|
28
|
+
* Foldkit's Vite plugin supplies the installed overlay automatically. It is
|
|
29
|
+
* included in production when `@foldkit/devtools` is a regular dependency.
|
|
30
|
+
*/
|
|
31
|
+
export type DevToolsOverlay = (store: DevToolsStore, position: DevToolsPosition, mode: DevToolsMode, maybeBanner: Option.Option<string>) => Effect.Effect<void, never, Scope.Scope>;
|
|
32
|
+
/**
|
|
33
|
+
* DevTools configuration.
|
|
34
|
+
*
|
|
35
|
+
* Pass `false` to disable DevTools entirely.
|
|
36
|
+
*
|
|
37
|
+
* - `show`: `'Development'` (default) enables in dev mode only, `'Always'` enables in all environments including production.
|
|
38
|
+
* - `position`: Where the badge and panel appear. Defaults to `'BottomRight'`.
|
|
39
|
+
* - `mode`: `'TimeTravel'` (default) enables full time-travel debugging by installing a paused historical view while the live application continues. `'Inspect'` allows browsing state snapshots without replacing the live view. Pass `{ development, production }` to use different modes per environment. Useful when DevTools is shown in production (`show: 'Always'`) and you want `'TimeTravel'` only in local development.
|
|
40
|
+
* - `banner`: Optional text shown as a banner at the top of the panel.
|
|
41
|
+
* - `excludeFromHistory`: Message `_tag` values whose dispatches should not be recorded in DevTools history. The Messages still drive `update` and the runtime as usual; they just don't appear in the history panel and don't pay the per-Message diff cost. Use for high-frequency Messages (animation frames, pointer moves, scroll events) that would flood history without adding insight.
|
|
42
|
+
* - `maxEntries`: Maximum number of recorded Messages retained in history before the oldest is evicted. Defaults to 100. Clamped to the range 20-500: smaller values keep the panel snappy under high message rates, larger values give you more scroll-back. Each retained entry stores a full Model snapshot, so memory cost scales linearly with both `maxEntries` and your Model size.
|
|
43
|
+
* - `keyframeInterval`: Number of recorded Messages between full Model snapshots. Defaults to 31. Time-travel to an index replays `update` forward from the nearest earlier keyframe, so this is a memory/time tradeoff: smaller values store more snapshots (more memory) but make each jump cheaper, down to `1` where every jump is a constant-time snapshot lookup with no replay. Reach for a denser interval when the app has a heavy `update` and time-travel jumps feel sluggish. Clamped to a minimum of 1. Forced to 1 automatically when `excludeFromHistory` is active, since excluded Messages are never replayed.
|
|
44
|
+
*/
|
|
45
|
+
export type DevToolsConfig = false | Readonly<{
|
|
46
|
+
show?: Visibility;
|
|
47
|
+
position?: DevToolsPosition;
|
|
48
|
+
mode?: DevToolsModeConfig;
|
|
49
|
+
banner?: string;
|
|
50
|
+
excludeFromHistory?: ReadonlyArray<string>;
|
|
51
|
+
maxEntries?: number;
|
|
52
|
+
keyframeInterval?: number;
|
|
53
|
+
/**
|
|
54
|
+
* The application's `Message` Schema. When provided and the running app
|
|
55
|
+
* is connected to the Foldkit DevTools MCP server, AI agents can dispatch
|
|
56
|
+
* Messages into the runtime. The Schema decodes inbound dispatch payloads
|
|
57
|
+
* at the bridge boundary and returns a clean error on mismatch.
|
|
58
|
+
*
|
|
59
|
+
* Without this field, `RequestDispatchMessage` is rejected with an
|
|
60
|
+
* informative error.
|
|
61
|
+
*/
|
|
62
|
+
Message?: Schema.Codec<any, any, unknown, unknown>;
|
|
63
|
+
}>;
|
|
64
|
+
/** Registers the overlay supplied by the Foldkit Vite plugin. */
|
|
65
|
+
export declare const __setDevToolsOverlay: (overlay: DevToolsOverlay | undefined) => void;
|
|
66
|
+
/** The DevTools settings a runtime boots with once `show` has been resolved
|
|
67
|
+
* against the environment. */
|
|
68
|
+
export type ResolvedDevToolsConfig = Readonly<{
|
|
69
|
+
position: DevToolsPosition;
|
|
70
|
+
mode: DevToolsMode;
|
|
71
|
+
maybeBanner: Option.Option<string>;
|
|
72
|
+
maybeOverlay: Option.Option<DevToolsOverlay>;
|
|
73
|
+
}>;
|
|
74
|
+
/** Resolves the DevTools config for this boot, or `Option.none()` when
|
|
75
|
+
* DevTools is disabled or hidden. `'Development'` visibility requires Vite
|
|
76
|
+
* HMR and a top-level window, so an app previewed inside an iframe does not
|
|
77
|
+
* stack a second panel over its host's. */
|
|
78
|
+
export declare const resolveDevToolsConfig: (devTools: DevToolsConfig | undefined) => Option.Option<ResolvedDevToolsConfig>;
|
|
79
|
+
/** The Message tags `excludeFromHistory` keeps out of the DevTools history. */
|
|
80
|
+
export declare const resolveExcludeFromHistoryTags: (devTools: DevToolsConfig | undefined) => ReadonlySet<string>;
|
|
81
|
+
/** The configured `maxEntries`, clamped to the supported range. */
|
|
82
|
+
export declare const resolveDevToolsMaxEntries: (devTools: DevToolsConfig | undefined) => number | undefined;
|
|
83
|
+
/** The configured `keyframeInterval`, floored and clamped to its minimum. */
|
|
84
|
+
export declare const resolveDevToolsKeyframeInterval: (devTools: DevToolsConfig | undefined) => number | undefined;
|
|
85
|
+
//# sourceMappingURL=devToolsConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devToolsConfig.d.ts","sourceRoot":"","sources":["../../src/runtime/devToolsConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAQ,MAAM,QAAQ,CAAA;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,iBAAiB,CAAA;AAE5D,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GAC1B,aAAa,GAAG,YAAY,GAAG,UAAU,GAAG,SAAS,CAAA;AAEvD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,YAAY,CAAA;AAEnD;;;;gFAIgF;AAChF,MAAM,MAAM,kBAAkB,GAC1B,YAAY,GACZ,QAAQ,CAAC;IAAE,WAAW,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,YAAY,CAAA;CAAE,CAAC,CAAA;AAErE;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAC/B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;AAE5C;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,QAAQ,CAAC;IACP,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,IAAI,CAAC,EAAE,kBAAkB,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kBAAkB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;CACnD,CAAC,CAAA;AAIN,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,YACtB,eAAe,GAAG,SAAS,KACnC,IAEF,CAAA;AAiBD;+BAC+B;AAC/B,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,IAAI,EAAE,YAAY,CAAA;IAClB,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAClC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;CAC7C,CAAC,CAAA;AAEF;;;4CAG4C;AAC5C,eAAO,MAAM,qBAAqB,aACtB,cAAc,GAAG,SAAS,KACnC,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAmBtC,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,6BAA6B,aAC9B,cAAc,GAAG,SAAS,KACnC,WAAW,CAAC,MAAM,CASlB,CAAA;AAEH,mEAAmE;AACnE,eAAO,MAAM,yBAAyB,aAC1B,cAAc,GAAG,SAAS,KACnC,MAAM,GAAG,SAaT,CAAA;AAEH,6EAA6E;AAC7E,eAAO,MAAM,+BAA+B,aAChC,cAAc,GAAG,SAAS,KACnC,MAAM,GAAG,SAUT,CAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Option, pipe } from 'effect';
|
|
2
|
+
import { isVisible } from './visibility.js';
|
|
3
|
+
let registeredDevToolsOverlay;
|
|
4
|
+
/** Registers the overlay supplied by the Foldkit Vite plugin. */
|
|
5
|
+
export const __setDevToolsOverlay = (overlay) => {
|
|
6
|
+
registeredDevToolsOverlay = overlay;
|
|
7
|
+
};
|
|
8
|
+
const DEFAULT_DEV_TOOLS_SHOW = 'Development';
|
|
9
|
+
const DEFAULT_DEV_TOOLS_POSITION = 'BottomRight';
|
|
10
|
+
const DEFAULT_DEV_TOOLS_MODE = 'TimeTravel';
|
|
11
|
+
const resolveDevToolsMode = (config) => {
|
|
12
|
+
if (typeof config === 'string') {
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
return import.meta.hot ? config.development : config.production;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const DEV_TOOLS_MAX_ENTRIES_MIN = 20;
|
|
20
|
+
const DEV_TOOLS_MAX_ENTRIES_MAX = 500;
|
|
21
|
+
const DEV_TOOLS_KEYFRAME_INTERVAL_MIN = 1;
|
|
22
|
+
/** Resolves the DevTools config for this boot, or `Option.none()` when
|
|
23
|
+
* DevTools is disabled or hidden. `'Development'` visibility requires Vite
|
|
24
|
+
* HMR and a top-level window, so an app previewed inside an iframe does not
|
|
25
|
+
* stack a second panel over its host's. */
|
|
26
|
+
export const resolveDevToolsConfig = (devTools) => {
|
|
27
|
+
const isInIframe = window.self !== window.top;
|
|
28
|
+
return pipe(devTools ?? {}, Option.liftPredicate(config => config !== false), Option.filter(config => isVisible(config.show ?? DEFAULT_DEV_TOOLS_SHOW, !!import.meta.hot && !isInIframe)), Option.map(config => ({
|
|
29
|
+
position: config.position ?? DEFAULT_DEV_TOOLS_POSITION,
|
|
30
|
+
mode: resolveDevToolsMode(config.mode ?? DEFAULT_DEV_TOOLS_MODE),
|
|
31
|
+
maybeBanner: Option.fromNullishOr(config.banner),
|
|
32
|
+
maybeOverlay: Option.fromNullishOr(registeredDevToolsOverlay),
|
|
33
|
+
})));
|
|
34
|
+
};
|
|
35
|
+
/** The Message tags `excludeFromHistory` keeps out of the DevTools history. */
|
|
36
|
+
export const resolveExcludeFromHistoryTags = (devTools) => pipe(devTools ?? {}, Option.liftPredicate(config => config !== false), Option.flatMapNullishOr(config => config.excludeFromHistory), Option.match({
|
|
37
|
+
onNone: () => new Set(),
|
|
38
|
+
onSome: tags => new Set(tags),
|
|
39
|
+
}));
|
|
40
|
+
/** The configured `maxEntries`, clamped to the supported range. */
|
|
41
|
+
export const resolveDevToolsMaxEntries = (devTools) => pipe(devTools ?? {}, Option.liftPredicate(config => config !== false), Option.flatMapNullishOr(config => config.maxEntries), Option.match({
|
|
42
|
+
onNone: () => undefined,
|
|
43
|
+
onSome: value => Math.max(DEV_TOOLS_MAX_ENTRIES_MIN, Math.min(DEV_TOOLS_MAX_ENTRIES_MAX, value)),
|
|
44
|
+
}));
|
|
45
|
+
/** The configured `keyframeInterval`, floored and clamped to its minimum. */
|
|
46
|
+
export const resolveDevToolsKeyframeInterval = (devTools) => pipe(devTools ?? {}, Option.liftPredicate(config => config !== false), Option.flatMapNullishOr(config => config.keyframeInterval), Option.match({
|
|
47
|
+
onNone: () => undefined,
|
|
48
|
+
onSome: value => Math.max(DEV_TOOLS_KEYFRAME_INTERVAL_MIN, Math.floor(value)),
|
|
49
|
+
}));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Effect, type Scope } from 'effect';
|
|
2
|
+
import { type MountRecord } from '../devTools/store.js';
|
|
3
|
+
import { MountRuntime, MountTracker, type ViewState } from '../mount/index.js';
|
|
4
|
+
import { type DevToolsConfig } from './devToolsConfig.js';
|
|
5
|
+
/** The Mount starts and ends recorded since the last drain. */
|
|
6
|
+
export type MountEvents = Readonly<{
|
|
7
|
+
starts: ReadonlyArray<MountRecord>;
|
|
8
|
+
ends: ReadonlyArray<MountRecord>;
|
|
9
|
+
}>;
|
|
10
|
+
/** What the DevTools history keeps about a Command: its name and arguments. */
|
|
11
|
+
export type RecordableCommand = Readonly<{
|
|
12
|
+
name: string;
|
|
13
|
+
args?: Record<string, unknown>;
|
|
14
|
+
}>;
|
|
15
|
+
/** The replay bridge the render side hands the store when it is installed. */
|
|
16
|
+
export type DevToolsRenderBridge = Readonly<{
|
|
17
|
+
renderReplay: (model: unknown) => Effect.Effect<void>;
|
|
18
|
+
markRenderPending: Effect.Effect<void>;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Everything the runtime needs from DevTools, whether or not DevTools is
|
|
22
|
+
* configured. The Mount tracker and runtime, the view state, and the pause
|
|
23
|
+
* check exist from boot, and the tracker records only once the store
|
|
24
|
+
* exists. `installDevToolsStore` creates the store, mounts the overlay,
|
|
25
|
+
* and starts the WebSocket bridge once the render side can supply the
|
|
26
|
+
* replay bridge, and does nothing when DevTools is off. The recording
|
|
27
|
+
* helpers no-op until the store exists.
|
|
28
|
+
*/
|
|
29
|
+
export type DevToolsIntegration<Model, Message> = Readonly<{
|
|
30
|
+
mountTracker: typeof MountTracker.Service;
|
|
31
|
+
mountRuntime: typeof MountRuntime.Service;
|
|
32
|
+
drainMountEvents: () => MountEvents;
|
|
33
|
+
readViewState: () => ViewState;
|
|
34
|
+
setViewState: (nextViewState: ViewState) => void;
|
|
35
|
+
isPausedNow: () => boolean;
|
|
36
|
+
installDevToolsStore: (bridge: DevToolsRenderBridge) => Effect.Effect<void, never, Scope.Scope>;
|
|
37
|
+
resumeDevTools: Effect.Effect<void>;
|
|
38
|
+
recordInit: (initModel: Model, initCommands: ReadonlyArray<RecordableCommand>) => Effect.Effect<void>;
|
|
39
|
+
recordMessage: (message: Message, currentModel: Model, nextModel: Model, commands: ReadonlyArray<RecordableCommand>) => void;
|
|
40
|
+
attachRenderedMounts: () => void;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Builds the DevTools integration for one runtime from the `devTools`
|
|
44
|
+
* config. `update` and `maybeFreezeModel` drive time-travel replay, and
|
|
45
|
+
* `enqueueMessageEffect` is how Messages sent over the WebSocket bridge
|
|
46
|
+
* reach the queue.
|
|
47
|
+
*/
|
|
48
|
+
export declare const makeDevToolsIntegration: <Model, Message>({ devTools, update, maybeFreezeModel, enqueueMessageEffect, }: Readonly<{
|
|
49
|
+
devTools: DevToolsConfig | undefined;
|
|
50
|
+
update: (model: Model, message: Message) => Readonly<{
|
|
51
|
+
model: Model;
|
|
52
|
+
}>;
|
|
53
|
+
maybeFreezeModel: (model: Model) => Model;
|
|
54
|
+
enqueueMessageEffect: (message: Message) => Effect.Effect<void>;
|
|
55
|
+
}>) => Effect.Effect<DevToolsIntegration<Model, Message>>;
|
|
56
|
+
//# sourceMappingURL=devToolsIntegration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devToolsIntegration.d.ts","sourceRoot":"","sources":["../../src/runtime/devToolsIntegration.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,MAAM,EAIN,KAAK,KAAK,EAGX,MAAM,QAAQ,CAAA;AAEf,OAAO,EAGL,KAAK,WAAW,EAEjB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,KAAK,cAAc,EAKpB,MAAM,qBAAqB,CAAA;AAE5B,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,MAAM,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IAClC,IAAI,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;CACjC,CAAC,CAAA;AAEF,+EAA+E;AAC/E,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B,CAAC,CAAA;AAEF,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACrD,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACvC,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACzD,YAAY,EAAE,OAAO,YAAY,CAAC,OAAO,CAAA;IACzC,YAAY,EAAE,OAAO,YAAY,CAAC,OAAO,CAAA;IACzC,gBAAgB,EAAE,MAAM,WAAW,CAAA;IACnC,aAAa,EAAE,MAAM,SAAS,CAAA;IAC9B,YAAY,EAAE,CAAC,aAAa,EAAE,SAAS,KAAK,IAAI,CAAA;IAChD,WAAW,EAAE,MAAM,OAAO,CAAA;IAC1B,oBAAoB,EAAE,CACpB,MAAM,EAAE,oBAAoB,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;IAC5C,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnC,UAAU,EAAE,CACV,SAAS,EAAE,KAAK,EAChB,YAAY,EAAE,aAAa,CAAC,iBAAiB,CAAC,KAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxB,aAAa,EAAE,CACb,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,KAAK,EACnB,SAAS,EAAE,KAAK,EAChB,QAAQ,EAAE,aAAa,CAAC,iBAAiB,CAAC,KACvC,IAAI,CAAA;IACT,oBAAoB,EAAE,MAAM,IAAI,CAAA;CACjC,CAAC,CAAA;AAOF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,EAAE,OAAO,iEAKnD,QAAQ,CAAC;IACV,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAC;QAAE,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAA;IACtE,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,KAAK,CAAA;IACzC,oBAAoB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAChE,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC,CAuOlD,CAAA"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Array, Effect, Option, PubSub, Stream, SubscriptionRef, } from 'effect';
|
|
2
|
+
import { createDevToolsStore, } from '../devTools/store.js';
|
|
3
|
+
import { startWebSocketBridge } from '../devTools/webSocketBridge.js';
|
|
4
|
+
import { MountRuntime, liveViewStateChanges, } from '../mount/index.js';
|
|
5
|
+
import { resolveDevToolsConfig, resolveDevToolsKeyframeInterval, resolveDevToolsMaxEntries, resolveExcludeFromHistoryTags, } from './devToolsConfig.js';
|
|
6
|
+
const toCommandRecord = (command) => command.args !== undefined
|
|
7
|
+
? { name: command.name, args: command.args }
|
|
8
|
+
: { name: command.name };
|
|
9
|
+
/**
|
|
10
|
+
* Builds the DevTools integration for one runtime from the `devTools`
|
|
11
|
+
* config. `update` and `maybeFreezeModel` drive time-travel replay, and
|
|
12
|
+
* `enqueueMessageEffect` is how Messages sent over the WebSocket bridge
|
|
13
|
+
* reach the queue.
|
|
14
|
+
*/
|
|
15
|
+
export const makeDevToolsIntegration = ({ devTools, update, maybeFreezeModel, enqueueMessageEffect, }) => Effect.gen(function* () {
|
|
16
|
+
const resolvedDevTools = resolveDevToolsConfig(devTools);
|
|
17
|
+
const excludeFromHistoryTags = resolveExcludeFromHistoryTags(devTools);
|
|
18
|
+
const devToolsMaxEntries = resolveDevToolsMaxEntries(devTools);
|
|
19
|
+
const devToolsKeyframeInterval = resolveDevToolsKeyframeInterval(devTools);
|
|
20
|
+
let currentViewState = 'Live';
|
|
21
|
+
const maybeViewStatePubSub = Option.isSome(resolvedDevTools)
|
|
22
|
+
? Option.some(yield* PubSub.unbounded({
|
|
23
|
+
replay: 1,
|
|
24
|
+
}))
|
|
25
|
+
: Option.none();
|
|
26
|
+
const viewStateChanges = Option.match(maybeViewStatePubSub, {
|
|
27
|
+
onNone: () => liveViewStateChanges,
|
|
28
|
+
onSome: viewStatePubSub => {
|
|
29
|
+
PubSub.publishUnsafe(viewStatePubSub, currentViewState);
|
|
30
|
+
return Stream.fromPubSub(viewStatePubSub);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const setViewState = (nextViewState) => {
|
|
34
|
+
if (nextViewState === currentViewState) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
currentViewState = nextViewState;
|
|
38
|
+
if (Option.isSome(maybeViewStatePubSub)) {
|
|
39
|
+
PubSub.publishUnsafe(maybeViewStatePubSub.value, nextViewState);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const readViewState = () => currentViewState;
|
|
43
|
+
// NOTE: the DevTools store is installed at most once during boot and
|
|
44
|
+
// never replaced. Caching it in a closure variable avoids a
|
|
45
|
+
// `Ref.get` on every message and on every render frame (the
|
|
46
|
+
// store powers the pause check). Plain `null` rather than `Option`:
|
|
47
|
+
// the hot path only ever presence-checks it, and the check should
|
|
48
|
+
// stay a bare comparison.
|
|
49
|
+
let devToolsStore = null;
|
|
50
|
+
const mountRuntime = MountRuntime.of({
|
|
51
|
+
captureViewStateChanges: () => Stream.concat(Stream.make(currentViewState), viewStateChanges).pipe(Stream.changes),
|
|
52
|
+
});
|
|
53
|
+
const isPausedNow = () => devToolsStore !== null &&
|
|
54
|
+
SubscriptionRef.getUnsafe(devToolsStore.stateRef).isPaused;
|
|
55
|
+
// NOTE: recording is gated on the DevTools store because the store
|
|
56
|
+
// is the only consumer. Without the gate every Mount start and end
|
|
57
|
+
// in a production frame would allocate a record just to be sliced
|
|
58
|
+
// and dropped.
|
|
59
|
+
const mountStartBuffer = [];
|
|
60
|
+
const mountEndBuffer = [];
|
|
61
|
+
const mountTracker = {
|
|
62
|
+
started: (name, args) => {
|
|
63
|
+
if (devToolsStore === null) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
mountStartBuffer.push(args === undefined ? { name } : { name, args });
|
|
67
|
+
},
|
|
68
|
+
ended: (name, args) => {
|
|
69
|
+
if (devToolsStore === null) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
mountEndBuffer.push(args === undefined ? { name } : { name, args });
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const drainMountEvents = () => {
|
|
76
|
+
const starts = mountStartBuffer.slice();
|
|
77
|
+
const ends = mountEndBuffer.slice();
|
|
78
|
+
mountStartBuffer.length = 0;
|
|
79
|
+
mountEndBuffer.length = 0;
|
|
80
|
+
return { starts, ends };
|
|
81
|
+
};
|
|
82
|
+
const installDevToolsStore = ({ renderReplay, markRenderPending, }) => Option.match(resolvedDevTools, {
|
|
83
|
+
onNone: () => Effect.void,
|
|
84
|
+
onSome: ({ position, mode, maybeBanner, maybeOverlay }) => Effect.gen(function* () {
|
|
85
|
+
// NOTE: when excludeFromHistory is active, the runtime drops
|
|
86
|
+
// excluded Messages from the recorded history. Replay walks the
|
|
87
|
+
// recorded entries forward from the nearest keyframe. With
|
|
88
|
+
// exclusion, the dropped Messages aren't in that walk, so any
|
|
89
|
+
// cumulative state they would have produced is missing from the
|
|
90
|
+
// replayed model. Setting keyframeInterval to 1 stores a full
|
|
91
|
+
// snapshot on every recorded entry, so time-travel becomes a
|
|
92
|
+
// direct lookup that reflects the real live state at the moment
|
|
93
|
+
// the entry was recorded.
|
|
94
|
+
const isExcludingMessages = excludeFromHistoryTags.size > 0;
|
|
95
|
+
const store = yield* createDevToolsStore({
|
|
96
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
97
|
+
replay: (model, message) => {
|
|
98
|
+
const replayUpdate = update(model, message);
|
|
99
|
+
return maybeFreezeModel(replayUpdate.model);
|
|
100
|
+
},
|
|
101
|
+
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
102
|
+
render: renderReplay,
|
|
103
|
+
markRenderPending,
|
|
104
|
+
}, {
|
|
105
|
+
...(devToolsKeyframeInterval !== undefined && {
|
|
106
|
+
keyframeInterval: devToolsKeyframeInterval,
|
|
107
|
+
}),
|
|
108
|
+
...(devToolsMaxEntries !== undefined && {
|
|
109
|
+
maxEntries: devToolsMaxEntries,
|
|
110
|
+
}),
|
|
111
|
+
// NOTE: exclusion forces keyframeInterval to 1 regardless of any
|
|
112
|
+
// configured value, since excluded Messages are never replayed
|
|
113
|
+
// and a denser interval would leave gaps in the replayed model.
|
|
114
|
+
// Spread last so it wins over `keyframeInterval` above.
|
|
115
|
+
...(isExcludingMessages && { keyframeInterval: 1 }),
|
|
116
|
+
});
|
|
117
|
+
devToolsStore = store;
|
|
118
|
+
// NOTE: the boot records init through `recordInit` after the init
|
|
119
|
+
// render, so the mount buffer reflects the Mounts that fired on the
|
|
120
|
+
// first paint.
|
|
121
|
+
yield* Option.match(maybeOverlay, {
|
|
122
|
+
onNone: () => Effect.void,
|
|
123
|
+
onSome: overlay => overlay(store, position, mode, maybeBanner),
|
|
124
|
+
});
|
|
125
|
+
if (import.meta.hot) {
|
|
126
|
+
const maybeMessageSchema = devTools !== undefined && devTools !== false
|
|
127
|
+
? Option.fromNullishOr(devTools.Message)
|
|
128
|
+
: Option.none();
|
|
129
|
+
yield* startWebSocketBridge(store, import.meta.hot,
|
|
130
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
131
|
+
message => enqueueMessageEffect(message),
|
|
132
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
133
|
+
maybeMessageSchema);
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
const recordInit = (initModel, initCommands) => Effect.gen(function* () {
|
|
138
|
+
const initMountEvents = drainMountEvents();
|
|
139
|
+
if (devToolsStore !== null) {
|
|
140
|
+
yield* devToolsStore.recordInit(initModel, Array.map(initCommands, toCommandRecord), initMountEvents.starts);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const attachRenderedMounts = () => {
|
|
144
|
+
if (devToolsStore !== null) {
|
|
145
|
+
const mountEvents = drainMountEvents();
|
|
146
|
+
Effect.runFork(devToolsStore.attachRenderedMounts(mountEvents.starts, mountEvents.ends));
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const recordMessage = (message, currentModel, nextModel, commands) => {
|
|
150
|
+
// NOTE: store writes go through `Effect.runFork`, not
|
|
151
|
+
// `Effect.runSync`. Both complete inline when the store's state
|
|
152
|
+
// Ref is uncontended (the always case on this path), but a
|
|
153
|
+
// DevTools fiber holding the Ref's permit across a yield would
|
|
154
|
+
// make `runSync` throw and crash the app; `runFork` parks and
|
|
155
|
+
// finishes the write when the permit frees, and the Ref's FIFO
|
|
156
|
+
// permit queue preserves write order.
|
|
157
|
+
if (devToolsStore !== null) {
|
|
158
|
+
const store = devToolsStore;
|
|
159
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
160
|
+
const tag = message._tag;
|
|
161
|
+
const isModelChanged = currentModel !== nextModel;
|
|
162
|
+
if (!excludeFromHistoryTags.has(tag)) {
|
|
163
|
+
Effect.runFork(store.recordMessage(
|
|
164
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
165
|
+
message, currentModel, nextModel, Array.map(commands, toCommandRecord), isModelChanged));
|
|
166
|
+
}
|
|
167
|
+
else if (isModelChanged) {
|
|
168
|
+
Effect.runFork(store.updateLatestModel(nextModel));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const resumeDevTools = Effect.suspend(() => devToolsStore !== null ? devToolsStore.resume : Effect.void);
|
|
173
|
+
return {
|
|
174
|
+
mountTracker,
|
|
175
|
+
mountRuntime,
|
|
176
|
+
drainMountEvents,
|
|
177
|
+
readViewState,
|
|
178
|
+
setViewState,
|
|
179
|
+
isPausedNow,
|
|
180
|
+
installDevToolsStore,
|
|
181
|
+
resumeDevTools,
|
|
182
|
+
recordInit,
|
|
183
|
+
recordMessage,
|
|
184
|
+
attachRenderedMounts,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context, Effect } from 'effect';
|
|
2
|
+
declare const Dispatch_base: Context.ServiceClass<Dispatch, "@foldkit/Dispatch", {
|
|
3
|
+
readonly dispatchAsync: (message: unknown) => Effect.Effect<void>;
|
|
4
|
+
readonly dispatchSync: (message: unknown) => void;
|
|
5
|
+
}>;
|
|
6
|
+
/** Effect service tag that provides message dispatching to the view layer. */
|
|
7
|
+
export declare class Dispatch extends Dispatch_base {
|
|
8
|
+
}
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/runtime/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;;4BAMZ,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;2BAC1C,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;;AALrD,8EAA8E;AAC9E,qBAAa,QAAS,SAAQ,aAMN;CAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documentMetadata.d.ts","sourceRoot":"","sources":["../../src/runtime/documentMetadata.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAA4B,MAAM,kBAAkB,CAAA;AAsLrE,eAAO,MAAM,qBAAqB,iBAClB,QAAQ,eACT,IAAI,GAAG,SAAS,KAC5B,IA8CF,CAAA"}
|