foldkit 0.82.6 → 0.82.8
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/runtime/hmrProtocol.d.ts +2 -1
- package/dist/runtime/hmrProtocol.d.ts.map +1 -1
- package/dist/runtime/hmrProtocol.js +2 -1
- package/dist/runtime/preserveScheduler.d.ts +29 -0
- package/dist/runtime/preserveScheduler.d.ts.map +1 -0
- package/dist/runtime/preserveScheduler.js +36 -0
- package/dist/runtime/renderLoop.d.ts +27 -0
- package/dist/runtime/renderLoop.d.ts.map +1 -0
- package/dist/runtime/renderLoop.js +25 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +92 -42
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Schema as S } from 'effect';
|
|
2
|
-
/** Browser → plugin: the runtime preserves its current Model under a stable id so it can be restored after a full reload. */
|
|
2
|
+
/** Browser → plugin: the runtime preserves its current Model under a stable id so it can be restored after a full reload. `isHmrReload` is set by the runtime when flushing on `vite:beforeFullReload` to mark the entry as eligible for restoration; absent or `false` for ordinary debounced preserves. */
|
|
3
3
|
export declare const PreserveModelMessage: S.Struct<{
|
|
4
4
|
readonly id: S.String;
|
|
5
5
|
readonly model: S.Unknown;
|
|
6
|
+
readonly isHmrReload: S.optional<S.Boolean>;
|
|
6
7
|
}>;
|
|
7
8
|
/** Browser → plugin: the runtime preserves its current Model under a stable id. */
|
|
8
9
|
export type PreserveModelMessage = typeof PreserveModelMessage.Type;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hmrProtocol.d.ts","sourceRoot":"","sources":["../../src/runtime/hmrProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEpC,
|
|
1
|
+
{"version":3,"file":"hmrProtocol.d.ts","sourceRoot":"","sources":["../../src/runtime/hmrProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEpC,6SAA6S;AAC7S,eAAO,MAAM,oBAAoB;;;;EAI/B,CAAA;AACF,mFAAmF;AACnF,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,sGAAsG;AACtG,eAAO,MAAM,mBAAmB;;EAE9B,CAAA;AACF,wFAAwF;AACxF,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAC,IAAI,CAAA;AAEjE,kKAAkK;AAClK,eAAO,MAAM,mBAAmB;;;EAG9B,CAAA;AACF,uFAAuF;AACvF,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAC,IAAI,CAAA"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Schema as S } from 'effect';
|
|
2
|
-
/** Browser → plugin: the runtime preserves its current Model under a stable id so it can be restored after a full reload. */
|
|
2
|
+
/** Browser → plugin: the runtime preserves its current Model under a stable id so it can be restored after a full reload. `isHmrReload` is set by the runtime when flushing on `vite:beforeFullReload` to mark the entry as eligible for restoration; absent or `false` for ordinary debounced preserves. */
|
|
3
3
|
export const PreserveModelMessage = S.Struct({
|
|
4
4
|
id: S.String,
|
|
5
5
|
model: S.Unknown,
|
|
6
|
+
isHmrReload: S.optional(S.Boolean),
|
|
6
7
|
});
|
|
7
8
|
/** Browser → plugin: the runtime requests its previously-preserved Model on startup, scoped by id. */
|
|
8
9
|
export const RequestModelMessage = S.Struct({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Duration, Effect, type Scope } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Defers a `preserve(model)` call so that bursts of dispatches do not pay the
|
|
4
|
+
* encoding cost on the hot path. `schedule` stashes the latest model and
|
|
5
|
+
* (re)starts a debounce timer; only one `onDebounce` runs per quiet window.
|
|
6
|
+
*
|
|
7
|
+
* `flush` runs `onFlush` with the latest pending model synchronously: it
|
|
8
|
+
* clears the pending slot atomically and invokes the callback, leaving any
|
|
9
|
+
* in-flight timer fiber to wake up, observe an empty pending slot, and exit
|
|
10
|
+
* as a no-op. Synchronous semantics let the runtime call this from
|
|
11
|
+
* `vite:beforeFullReload` via `Effect.runSync`, with no race against Vite's
|
|
12
|
+
* `location.reload()`. The two-callback split lets the flush path send
|
|
13
|
+
* `isHmrReload: true` while debounced calls send `false`.
|
|
14
|
+
*
|
|
15
|
+
* `cancel` drops the pending model and interrupts the in-flight fiber.
|
|
16
|
+
* Forked fibers are tied to the construction scope via `forkIn`, so scope
|
|
17
|
+
* closure interrupts them too. `cancel` covers manual teardown.
|
|
18
|
+
*/
|
|
19
|
+
export type PreserveScheduler<Model> = Readonly<{
|
|
20
|
+
schedule: (model: Model) => Effect.Effect<void>;
|
|
21
|
+
flush: Effect.Effect<void>;
|
|
22
|
+
cancel: Effect.Effect<void>;
|
|
23
|
+
}>;
|
|
24
|
+
export type PreserveSchedulerCallbacks<Model> = Readonly<{
|
|
25
|
+
onDebounce: (model: Model) => Effect.Effect<void>;
|
|
26
|
+
onFlush: (model: Model) => Effect.Effect<void>;
|
|
27
|
+
}>;
|
|
28
|
+
export declare const makePreserveScheduler: <Model>(callbacks: PreserveSchedulerCallbacks<Model>, delay: Duration.Input) => Effect.Effect<PreserveScheduler<Model>, never, Scope.Scope>;
|
|
29
|
+
//# sourceMappingURL=preserveScheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preserveScheduler.d.ts","sourceRoot":"","sources":["../../src/runtime/preserveScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,EAAsB,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAA;AAE9E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,iBAAiB,CAAC,KAAK,IAAI,QAAQ,CAAC;IAC9C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/C,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAC5B,CAAC,CAAA;AAEF,MAAM,MAAM,0BAA0B,CAAC,KAAK,IAAI,QAAQ,CAAC;IACvD,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACjD,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAC/C,CAAC,CAAA;AAEF,eAAO,MAAM,qBAAqB,GAAI,KAAK,EACzC,WAAW,0BAA0B,CAAC,KAAK,CAAC,EAC5C,OAAO,QAAQ,CAAC,KAAK,KACpB,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAgDzD,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect, Fiber, Option, Ref } from 'effect';
|
|
2
|
+
export const makePreserveScheduler = (callbacks, delay) => Effect.gen(function* () {
|
|
3
|
+
const scope = yield* Effect.scope;
|
|
4
|
+
const pendingRef = yield* Ref.make(Option.none());
|
|
5
|
+
const fiberRef = yield* Ref.make(Option.none());
|
|
6
|
+
const interruptPending = Effect.gen(function* () {
|
|
7
|
+
const maybeFiber = yield* Ref.get(fiberRef);
|
|
8
|
+
yield* Option.match(maybeFiber, {
|
|
9
|
+
onNone: () => Effect.void,
|
|
10
|
+
onSome: Fiber.interrupt,
|
|
11
|
+
});
|
|
12
|
+
yield* Ref.set(fiberRef, Option.none());
|
|
13
|
+
});
|
|
14
|
+
const runPending = (callback) => Effect.gen(function* () {
|
|
15
|
+
const maybePending = yield* Ref.getAndSet(pendingRef, Option.none());
|
|
16
|
+
yield* Option.match(maybePending, {
|
|
17
|
+
onNone: () => Effect.void,
|
|
18
|
+
onSome: callback,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const schedule = (model) => Effect.gen(function* () {
|
|
22
|
+
yield* Ref.set(pendingRef, Option.some(model));
|
|
23
|
+
yield* interruptPending;
|
|
24
|
+
const fiber = yield* Effect.forkIn(Effect.gen(function* () {
|
|
25
|
+
yield* Effect.sleep(delay);
|
|
26
|
+
yield* runPending(callbacks.onDebounce);
|
|
27
|
+
}), scope);
|
|
28
|
+
yield* Ref.set(fiberRef, Option.some(fiber));
|
|
29
|
+
});
|
|
30
|
+
const flush = runPending(callbacks.onFlush);
|
|
31
|
+
const cancel = Effect.gen(function* () {
|
|
32
|
+
yield* interruptPending;
|
|
33
|
+
yield* Ref.set(pendingRef, Option.none());
|
|
34
|
+
});
|
|
35
|
+
return { schedule, flush, cancel };
|
|
36
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, SubscriptionRef } from 'effect';
|
|
2
|
+
export type RenderLoopConfig = Readonly<{
|
|
3
|
+
/** SubscriptionRef holding the dirty bit. The loop subscribes to its changes
|
|
4
|
+
* Stream so it can suspend entirely when the bit stays false. */
|
|
5
|
+
pendingRef: SubscriptionRef.SubscriptionRef<boolean>;
|
|
6
|
+
/** Effect that yields once the next animation frame fires. */
|
|
7
|
+
awaitNextFrame: Effect.Effect<void>;
|
|
8
|
+
/** Effect that returns whether the runtime is paused (e.g. via DevTools
|
|
9
|
+
* time-travel). When true, the body skips render but still clears the bit. */
|
|
10
|
+
isPaused: Effect.Effect<boolean>;
|
|
11
|
+
/** Effect that performs one render with the latest model. */
|
|
12
|
+
render: Effect.Effect<void>;
|
|
13
|
+
}>;
|
|
14
|
+
/** Builds an idle-suspending render loop driven by a SubscriptionRef of
|
|
15
|
+
* "render is pending."
|
|
16
|
+
*
|
|
17
|
+
* The loop subscribes to the ref's changes Stream and runs the body once per
|
|
18
|
+
* false-to-true transition. Stream.changes filters consecutive equals so a
|
|
19
|
+
* burst of `set(true)` calls inside one frame produces a single body run.
|
|
20
|
+
*
|
|
21
|
+
* The body waits for the next animation frame, clears the dirty bit, then
|
|
22
|
+
* renders if not paused. The bit is cleared on every body run (including
|
|
23
|
+
* paused early-returns) so the next dispatch is always a real false-to-true
|
|
24
|
+
* transition. This makes the loop self-recovering even when paused state
|
|
25
|
+
* flips externally without a synchronous render. */
|
|
26
|
+
export declare const makeRenderLoop: ({ pendingRef, awaitNextFrame, isPaused, render, }: RenderLoopConfig) => Effect.Effect<void>;
|
|
27
|
+
//# sourceMappingURL=renderLoop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderLoop.d.ts","sourceRoot":"","sources":["../../src/runtime/renderLoop.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,eAAe,EAAE,MAAM,QAAQ,CAAA;AAExD,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC;sEACkE;IAClE,UAAU,EAAE,eAAe,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;IACpD,8DAA8D;IAC9D,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnC;mFAC+E;IAC/E,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAChC,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAC5B,CAAC,CAAA;AAEF;;;;;;;;;;;qDAWqD;AACrD,eAAO,MAAM,cAAc,GAAI,mDAK5B,gBAAgB,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAgBvC,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect, Stream, SubscriptionRef } from 'effect';
|
|
2
|
+
/** Builds an idle-suspending render loop driven by a SubscriptionRef of
|
|
3
|
+
* "render is pending."
|
|
4
|
+
*
|
|
5
|
+
* The loop subscribes to the ref's changes Stream and runs the body once per
|
|
6
|
+
* false-to-true transition. Stream.changes filters consecutive equals so a
|
|
7
|
+
* burst of `set(true)` calls inside one frame produces a single body run.
|
|
8
|
+
*
|
|
9
|
+
* The body waits for the next animation frame, clears the dirty bit, then
|
|
10
|
+
* renders if not paused. The bit is cleared on every body run (including
|
|
11
|
+
* paused early-returns) so the next dispatch is always a real false-to-true
|
|
12
|
+
* transition. This makes the loop self-recovering even when paused state
|
|
13
|
+
* flips externally without a synchronous render. */
|
|
14
|
+
export const makeRenderLoop = ({ pendingRef, awaitNextFrame, isPaused, render, }) => {
|
|
15
|
+
const tick = Effect.gen(function* () {
|
|
16
|
+
yield* awaitNextFrame;
|
|
17
|
+
yield* SubscriptionRef.set(pendingRef, false);
|
|
18
|
+
const paused = yield* isPaused;
|
|
19
|
+
if (paused) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
yield* render;
|
|
23
|
+
});
|
|
24
|
+
return SubscriptionRef.changes(pendingRef).pipe(Stream.changes, Stream.filter(isPending => isPending), Stream.runForEach(() => tick));
|
|
25
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,OAAO,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,OAAO,EAEP,MAAM,EAGN,KAAK,EAEL,MAAM,EAON,MAAM,EAIP,MAAM,QAAQ,CAAA;AAGf,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAIlD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,GAAG,EAA+B,MAAM,iBAAiB,CAAA;AAalE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,sBAAsB,CAAA;AAI7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAO5C,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,YAAY,GACZ,UAAU,GACV,SAAS,CAAA;AAEb,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,QAAQ,CAAA;AAEjD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,YAAY,CAAA;AAEnD;;;;;;;;;GASG;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,YAAY,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;CACnD,CAAC,CAAA;AAMN,sFAAsF;AACtF,MAAM,MAAM,eAAe,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACrD,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,EAAE,OAAO,IACrC,KAAK,GACL,QAAQ,CAAC;IACP,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CAChE,CAAC,CAAA;;4BA6BsB,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;AAE3B,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,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,0GAA0G;AAC1G,MAAM,MAAM,YAAY,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IAClD,KAAK,EAAE,KAAK,CAAA;IACZ,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;CACjB,CAAC,CAAA;AAEF,iFAAiF;AACjF,MAAM,MAAM,WAAW,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACjD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,QAAQ,CAAA;IAC1D,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CACzD,CAAC,CAAA;AAwEF,KAAK,iBAAiB,CACpB,KAAK,EACL,OAAO,EACP,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,IAC7B,QAAQ,CAAC;IACX,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACjD,MAAM,EAAE,CACN,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,KACb,SAAS;QACZ,KAAK;QACL,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAAC;KAC5E,CAAA;IACD,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,QAAQ,CAAA;IAChC,aAAa,CAAC,EAAE,aAAa,CAC3B,KAAK,EACL,OAAO,EACP,aAAa,EACb,SAAS,GAAG,uBAAuB,CACpC,CAAA;IACD,SAAS,EAAE,WAAW,CAAA;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACnC,QAAQ,CAAC,EAAE,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACzC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAClC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,CAAC,CAAA;IAC5E,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B,CAAC,CAAA;AAEF,kEAAkE;AAClE,MAAM,MAAM,6BAA6B,CACvC,KAAK,EACL,OAAO,EACP,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,KAAK,EACL,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,IAC7B,iBAAiB,CACnB,KAAK,EACL,OAAO,EACP,aAAa,EACb,SAAS,EACT,uBAAuB,CACxB,GACC,QAAQ,CAAC;IACP,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACjD,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC3B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,IAAI,EAAE,CACJ,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,GAAG,KACL,SAAS;QACZ,KAAK;QACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;KACF,CAAA;CACF,CAAC,CAAA;AAEJ,qEAAqE;AACrE,MAAM,MAAM,oBAAoB,CAC9B,KAAK,EACL,OAAO,EACP,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,IAC7B,iBAAiB,CACnB,KAAK,EACL,OAAO,EACP,aAAa,EACb,SAAS,EACT,uBAAuB,CACxB,GACC,QAAQ,CAAC;IACP,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,IAAI,EAAE,CACJ,GAAG,EAAE,GAAG,KACL,SAAS;QACZ,KAAK;QACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;KACF,CAAA;CACF,CAAC,CAAA;AAEJ,qEAAqE;AACrE,MAAM,MAAM,sBAAsB,CAChC,KAAK,EACL,OAAO,EACP,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,KAAK,EACL,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,IAC7B,iBAAiB,CACnB,KAAK,EACL,OAAO,EACP,aAAa,EACb,SAAS,EACT,uBAAuB,CACxB,GACC,QAAQ,CAAC;IACP,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACjD,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC3B,IAAI,EAAE,CACJ,KAAK,EAAE,KAAK,KACT,SAAS;QACZ,KAAK;QACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;KACF,CAAA;CACF,CAAC,CAAA;AAEJ,oEAAoE;AACpE,MAAM,MAAM,aAAa,CACvB,KAAK,EACL,OAAO,EACP,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,IAC7B,iBAAiB,CACnB,KAAK,EACL,OAAO,EACP,aAAa,EACb,SAAS,EACT,uBAAuB,CACxB,GACC,QAAQ,CAAC;IACP,IAAI,EAAE,MAAM,SAAS;QACnB,KAAK;QACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;KACF,CAAA;CACF,CAAC,CAAA;AAEJ,iEAAiE;AACjE,MAAM,MAAM,WAAW,CACrB,KAAK,EACL,OAAO,EACP,KAAK,GAAG,IAAI,EACZ,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,IAC7B,KAAK,SAAS,IAAI,GAClB,MAAM,SAAS;IACb,KAAK;IACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;CACF,GACD,CACE,KAAK,EAAE,KAAK,KACT,SAAS;IACZ,KAAK;IACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;CACF,CAAA;AAEL,2GAA2G;AAC3G,MAAM,MAAM,kBAAkB,CAC5B,KAAK,EACL,OAAO,EACP,KAAK,GAAG,IAAI,EACZ,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,IAC7B,KAAK,SAAS,IAAI,GAClB,CACE,GAAG,EAAE,GAAG,KACL,SAAS;IACZ,KAAK;IACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;CACF,GACD,CACE,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,GAAG,KACL,SAAS;IACZ,KAAK;IACL,aAAa,CACX,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAC7D;CACF,CAAA;AAEL,wGAAwG;AACxG,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACnD,CAAC,CAAA;AAw0BF,2HAA2H;AAC3H,wBAAgB,WAAW,CACzB,KAAK,EACL,OAAO,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAChC,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,KAAK,EACL,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,EAE/B,MAAM,EAAE,6BAA6B,CACnC,KAAK,EACL,OAAO,EACP,aAAa,EACb,KAAK,EACL,SAAS,EACT,uBAAuB,CACxB,GACA,iBAAiB,CAAA;AAEpB,wBAAgB,WAAW,CACzB,KAAK,EACL,OAAO,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAChC,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,EAE/B,MAAM,EAAE,oBAAoB,CAC1B,KAAK,EACL,OAAO,EACP,aAAa,EACb,SAAS,EACT,uBAAuB,CACxB,GACA,iBAAiB,CAAA;AAEpB,wBAAgB,WAAW,CACzB,KAAK,EACL,OAAO,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAChC,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,KAAK,EACL,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,EAE/B,MAAM,EAAE,sBAAsB,CAC5B,KAAK,EACL,OAAO,EACP,aAAa,EACb,KAAK,EACL,SAAS,EACT,uBAAuB,CACxB,GACA,iBAAiB,CAAA;AAEpB,wBAAgB,WAAW,CACzB,KAAK,EACL,OAAO,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAChC,aAAa,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACzD,SAAS,GAAG,KAAK,EACjB,uBAAuB,GAAG,KAAK,EAE/B,MAAM,EAAE,aAAa,CACnB,KAAK,EACL,OAAO,EACP,aAAa,EACb,SAAS,EACT,uBAAuB,CACxB,GACA,iBAAiB,CAAA;AA2NpB,kEAAkE;AAClE,eAAO,MAAM,GAAG,GAAI,SAAS,iBAAiB,KAAG,IA4ChD,CAAA"}
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BrowserRuntime } from '@effect/platform-browser';
|
|
2
|
-
import { Array, Cause, Context, Effect, Exit, Function, Layer, Match, Option, Predicate, PubSub, Queue, Record, Ref, Scheduler, Schema, Stream, SubscriptionRef, pipe, } from 'effect';
|
|
2
|
+
import { Array, Cause, Context, Duration, Effect, Exit, Function, Layer, Match, Option, Predicate, PubSub, Queue, Record, Ref, Scheduler, Schema, Stream, SubscriptionRef, pipe, } from 'effect';
|
|
3
3
|
import { h } from 'snabbdom';
|
|
4
4
|
import { createOverlay } from '../devTools/overlay.js';
|
|
5
5
|
import { createDevToolsStore } from '../devTools/store.js';
|
|
@@ -11,6 +11,8 @@ import { defaultCrashView, noOpDispatch } from './crashUI.js';
|
|
|
11
11
|
import { deepFreeze } from './deepFreeze.js';
|
|
12
12
|
import { PreserveModelMessage, RequestModelMessage, RestoreModelMessage, } from './hmrProtocol.js';
|
|
13
13
|
import { orderByPriority } from './messagePriority.js';
|
|
14
|
+
import { makePreserveScheduler } from './preserveScheduler.js';
|
|
15
|
+
import { makeRenderLoop } from './renderLoop.js';
|
|
14
16
|
const DEFAULT_DEV_TOOLS_SHOW = 'Development';
|
|
15
17
|
const DEFAULT_DEV_TOOLS_POSITION = 'BottomRight';
|
|
16
18
|
const DEFAULT_DEV_TOOLS_MODE = 'TimeTravel';
|
|
@@ -97,12 +99,40 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
97
99
|
});
|
|
98
100
|
};
|
|
99
101
|
const flags = yield* resolveFlags;
|
|
100
|
-
const modelEquivalence = Schema.toEquivalence(Model);
|
|
101
102
|
const ModelJsonCodec = Schema.toCodecJson(
|
|
102
103
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
103
104
|
Model);
|
|
104
105
|
const decodeHmrModel = Schema.decodeUnknownExit(ModelJsonCodec);
|
|
105
106
|
const encodeHmrModel = Schema.encodeUnknownSync(ModelJsonCodec);
|
|
107
|
+
// NOTE: keep `encodeHmrModel` off the dispatch hot path. It walks
|
|
108
|
+
// the entire Model graph (O(modelSize) per call) and blocks input
|
|
109
|
+
// on large Models. The scheduler defers encoding to a quiet window
|
|
110
|
+
// and the `vite:beforeFullReload` flush covers the HMR boundary.
|
|
111
|
+
const PRESERVE_DEBOUNCE = Duration.millis(200);
|
|
112
|
+
const preserveScheduler = yield* makePreserveScheduler({
|
|
113
|
+
onDebounce: model => Effect.sync(() => preserveModel(runtimeId, encodeHmrModel(model), false)),
|
|
114
|
+
onFlush: model => Effect.sync(() => preserveModel(runtimeId, encodeHmrModel(model), true)),
|
|
115
|
+
}, PRESERVE_DEBOUNCE);
|
|
116
|
+
const hot = import.meta.hot;
|
|
117
|
+
if (hot) {
|
|
118
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
119
|
+
// NOTE: Effect.runSync requires `flush` to have no async
|
|
120
|
+
// suspensions. The scheduler is built to satisfy that: flush
|
|
121
|
+
// clears pending atomically and runs `onFlush` without
|
|
122
|
+
// interrupting the in-flight timer fiber, which keeps the
|
|
123
|
+
// whole effect synchronous. If a future change adds an async
|
|
124
|
+
// step (interrupt-await, sleep, fork) on this path, Vite may
|
|
125
|
+
// race ahead to location.reload() before the encoded model
|
|
126
|
+
// reaches the plugin.
|
|
127
|
+
const handler = () => {
|
|
128
|
+
Effect.runSync(preserveScheduler.flush);
|
|
129
|
+
};
|
|
130
|
+
hot.on('vite:beforeFullReload', handler);
|
|
131
|
+
return handler;
|
|
132
|
+
}), handler => Effect.sync(() => hot.off('vite:beforeFullReload', handler)));
|
|
133
|
+
yield* Effect.addFinalizer(() => preserveScheduler.cancel);
|
|
134
|
+
}
|
|
135
|
+
const schedulePreserveModel = (model) => hot ? preserveScheduler.schedule(model) : Effect.void;
|
|
106
136
|
// NOTE: Each enqueued Message carries a priority. Within a single
|
|
107
137
|
// takeAll batch the drain loop processes all High before any Normal,
|
|
108
138
|
// so user input (view dispatch, navigation, subscription events,
|
|
@@ -159,10 +189,8 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
159
189
|
yield* Ref.set(modelRef, nextModel);
|
|
160
190
|
yield* SubscriptionRef.set(isRenderPendingRef, true);
|
|
161
191
|
yield* Ref.set(lastDirtyMessageRef, Option.some(message));
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
preserveModel(runtimeId, encodeHmrModel(nextModel));
|
|
165
|
-
}
|
|
192
|
+
PubSub.publishUnsafe(modelPubSub, nextModel);
|
|
193
|
+
yield* schedulePreserveModel(nextModel);
|
|
166
194
|
}
|
|
167
195
|
yield* Effect.forEach(
|
|
168
196
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
@@ -246,46 +274,39 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
246
274
|
// view past threshold. Acceptable for a debug callback; full
|
|
247
275
|
// attribution would require correlating each message with its render
|
|
248
276
|
// contribution, which isn't worth the complexity.
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
yield* render(model, maybeMessage);
|
|
277
|
+
const renderLoop = makeRenderLoop({
|
|
278
|
+
pendingRef: isRenderPendingRef,
|
|
279
|
+
awaitNextFrame,
|
|
280
|
+
isPaused: isPausedEffect,
|
|
281
|
+
render: Effect.gen(function* () {
|
|
282
|
+
const model = yield* Ref.get(modelRef);
|
|
283
|
+
const maybeMessage = yield* Ref.get(lastDirtyMessageRef);
|
|
284
|
+
yield* render(model, maybeMessage);
|
|
285
|
+
}),
|
|
259
286
|
});
|
|
260
|
-
// NOTE: The loop suspends on the SubscriptionRef's changes Stream when
|
|
261
|
-
// isRenderPending stays false, so an idle app schedules zero rAF
|
|
262
|
-
// callbacks. Stream.changes filters consecutive equals so multiple
|
|
263
|
-
// dispatches inside one frame produce a single render. The bit is
|
|
264
|
-
// cleared on every body run, including the paused early-return, so
|
|
265
|
-
// any future false-to-true transition wakes the loop again. This
|
|
266
|
-
// self-recovery covers indirect unpause paths in the DevTools store
|
|
267
|
-
// (rolling-buffer eviction past pausedAtIndex, and the clear action)
|
|
268
|
-
// that bypass bridge.render.
|
|
269
|
-
const renderLoop = SubscriptionRef.changes(isRenderPendingRef).pipe(Stream.changes, Stream.filter(isPending => isPending), Stream.runForEach(() => renderAtNextFrame));
|
|
270
287
|
yield* Effect.forkDetach(renderLoop);
|
|
271
288
|
addBfcacheRestoreListener();
|
|
272
289
|
if (subscriptions) {
|
|
273
|
-
yield* pipe(subscriptions, Record.toEntries, Effect.forEach(([_key, { schema, modelToDependencies, equivalence: customEquivalence, dependenciesToStream, },]) => {
|
|
274
|
-
|
|
290
|
+
yield* pipe(subscriptions, Record.toEntries, Effect.forEach(([_key, { schema, modelToDependencies, equivalence: customEquivalence, dependenciesToStream, },]) => Effect.gen(function* () {
|
|
291
|
+
const latestDependenciesRef = yield* Ref.make(modelToDependencies(initModel));
|
|
275
292
|
const equivalence = customEquivalence ?? Schema.toEquivalence(schema);
|
|
276
293
|
const modelStream = Stream.concat(Stream.make(initModel), Stream.fromPubSub(modelPubSub));
|
|
277
|
-
|
|
278
|
-
// NOTE:
|
|
279
|
-
// readDependencies() returns
|
|
280
|
-
//
|
|
281
|
-
|
|
294
|
+
yield* Effect.forkDetach(modelStream.pipe(
|
|
295
|
+
// NOTE: Ref.set runs upstream of Stream.changesWith on
|
|
296
|
+
// every model change, so readDependencies() returns
|
|
297
|
+
// current values even when the equivalence filter
|
|
298
|
+
// doesn't emit. Moving this into a tap after
|
|
299
|
+
// changesWith would silently break subscribers whose
|
|
300
|
+
// dependencies are equivalence-stable across model
|
|
301
|
+
// changes.
|
|
302
|
+
Stream.mapEffect(model => Effect.gen(function* () {
|
|
282
303
|
const dependencies = modelToDependencies(model);
|
|
283
|
-
|
|
304
|
+
yield* Ref.set(latestDependenciesRef, dependencies);
|
|
284
305
|
return dependencies;
|
|
285
|
-
}), Stream.changesWith(equivalence), Stream.switchMap(dependencies => dependenciesToStream(dependencies, () =>
|
|
306
|
+
})), Stream.changesWith(equivalence), Stream.switchMap(dependencies => dependenciesToStream(dependencies, () => Ref.getUnsafe(latestDependenciesRef))), Stream.runForEach(message =>
|
|
286
307
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
287
308
|
enqueueHigh(message)), provideAllResources));
|
|
288
|
-
}, {
|
|
309
|
+
}), {
|
|
289
310
|
concurrency: 'unbounded',
|
|
290
311
|
discard: true,
|
|
291
312
|
}));
|
|
@@ -333,18 +354,47 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
333
354
|
const processBatch = (batch) => Effect.forEach(orderByPriority(batch), processWithBudget, {
|
|
334
355
|
discard: true,
|
|
335
356
|
});
|
|
357
|
+
// NOTE: Effect 4's `Queue.takeAll` blocks until at least one message
|
|
358
|
+
// arrives (it's `takeBetween(self, 1, ∞)`, not a non-blocking
|
|
359
|
+
// snapshot). For batching we want "give me whatever is currently in
|
|
360
|
+
// the queue, possibly nothing" so we drain via repeated `Queue.poll`
|
|
361
|
+
// until it returns `None`.
|
|
362
|
+
const pollAvailable = Effect.gen(function* () {
|
|
363
|
+
const accumulated = [];
|
|
364
|
+
while (true) {
|
|
365
|
+
const next = yield* Queue.poll(messageQueue);
|
|
366
|
+
if (Option.isNone(next)) {
|
|
367
|
+
return accumulated;
|
|
368
|
+
}
|
|
369
|
+
accumulated.push(next.value);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
336
372
|
const drainQueue = Effect.gen(function* () {
|
|
337
|
-
const batch = yield*
|
|
373
|
+
const batch = yield* pollAvailable;
|
|
338
374
|
if (Array.isReadonlyArrayEmpty(batch)) {
|
|
339
375
|
return;
|
|
340
376
|
}
|
|
341
377
|
yield* processBatch(batch);
|
|
342
378
|
yield* drainQueue;
|
|
343
379
|
});
|
|
380
|
+
// NOTE: only reset the burst timer when `Queue.take` actually blocked
|
|
381
|
+
// (queue was empty). With Command-chained dispatches each forever
|
|
382
|
+
// iteration handles a single message, so resetting unconditionally
|
|
383
|
+
// would keep the per-iteration cost under FRAME_BUDGET_MS forever
|
|
384
|
+
// and the runtime would never yield to the browser. Polling first
|
|
385
|
+
// distinguishes "continuing a burst" (poll returns Some) from
|
|
386
|
+
// "waking from idle" (poll returns None, take blocks).
|
|
344
387
|
yield* pipe(Effect.forever(Effect.gen(function* () {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
388
|
+
const maybeFirst = yield* Queue.poll(messageQueue);
|
|
389
|
+
const first = yield* Option.match(maybeFirst, {
|
|
390
|
+
onNone: () => Effect.gen(function* () {
|
|
391
|
+
const message = yield* Queue.take(messageQueue);
|
|
392
|
+
yield* Ref.set(burstStartedAtRef, performance.now());
|
|
393
|
+
return message;
|
|
394
|
+
}),
|
|
395
|
+
onSome: Effect.succeed,
|
|
396
|
+
});
|
|
397
|
+
const rest = yield* pollAvailable;
|
|
348
398
|
yield* processBatch(Array.prepend(rest, first));
|
|
349
399
|
yield* drainQueue;
|
|
350
400
|
})), Effect.catchCause(cause => Effect.sync(() => {
|
|
@@ -492,9 +542,9 @@ export function makeProgram(config) {
|
|
|
492
542
|
const encodePreserveModelMessage = Schema.encodeUnknownSync(PreserveModelMessage);
|
|
493
543
|
const encodeRequestModelMessage = Schema.encodeUnknownSync(RequestModelMessage);
|
|
494
544
|
const decodeRestoreModelMessage = Schema.decodeUnknownExit(RestoreModelMessage);
|
|
495
|
-
const preserveModel = (id,
|
|
545
|
+
const preserveModel = (id, encodedModel, isHmrReload) => {
|
|
496
546
|
if (import.meta.hot) {
|
|
497
|
-
import.meta.hot.send('foldkit:preserve-model', encodePreserveModelMessage(PreserveModelMessage.make({ id, model })));
|
|
547
|
+
import.meta.hot.send('foldkit:preserve-model', encodePreserveModelMessage(PreserveModelMessage.make({ id, model: encodedModel, isHmrReload })));
|
|
498
548
|
}
|
|
499
549
|
};
|
|
500
550
|
const PLUGIN_RESPONSE_TIMEOUT_MS = 500;
|