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
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { Cause, Context, Effect, Exit, Option } from 'effect';
|
|
2
|
+
import { __beginRender as beginHtmlRender, __beginReplayRender as beginReplayHtmlRender, __clearRuntime as clearHtmlRuntime, __createBoundaryRegistry as createHtmlBoundaryRegistry, __endReplayRender as endReplayHtmlRender, __flushReplayUnmountsAfterPatchFailure as flushReplayUnmountsAfterPatchFailure, __setRuntime as setHtmlRuntime, } from '../html/index.js';
|
|
3
|
+
import { __hydrateVNode } from '../hydrate.js';
|
|
4
|
+
import { FOLDKIT_APP_ATTRIBUTE } from '../hydrationMarker.js';
|
|
5
|
+
import { MountRuntime, MountTracker } from '../mount/index.js';
|
|
6
|
+
import { __patchVNode, __recoverVNodeAfterPatchFailure, } from '../vdom.js';
|
|
7
|
+
import { noOpDispatch, renderCrashView, } from './crashUI.js';
|
|
8
|
+
import { Dispatch } from './dispatch.js';
|
|
9
|
+
import { applyDocumentMetadata } from './documentMetadata.js';
|
|
10
|
+
import { measureSlowPhase, reportSlowPhase, } from './slowPhase.js';
|
|
11
|
+
import { __decideViewTransition, __silenceViewTransitionRejections, } from './viewTransition.js';
|
|
12
|
+
/**
|
|
13
|
+
* Builds the render side of one runtime: the current vnode and, when
|
|
14
|
+
* hydrating, the server-rendered root the first render adopts, the crash
|
|
15
|
+
* renderer, the `Dispatch` service the view binds handlers to, the
|
|
16
|
+
* synchronous render that runs the view and patches the DOM, the render
|
|
17
|
+
* frame that runs in `requestAnimationFrame`, and the View Transition
|
|
18
|
+
* that frame may run inside. It also registers the teardown finalizer
|
|
19
|
+
* that patches the tree away and restores the container, so build it
|
|
20
|
+
* before forking the perpetual fibers: scope finalizers run
|
|
21
|
+
* last-registered first, and the container must be restored only after
|
|
22
|
+
* those fibers are interrupted.
|
|
23
|
+
*/
|
|
24
|
+
export const makeRenderer = ({ status, container, view, htmlBuilder, manageDocument, crash, buildId, initModel, maybeHydrationRoot, maybeSlowView, maybeSlowPatch, duplicateIdScanner, maybeResolvedViewTransition, commitNotifier, runtimeContext, readLiveModel, messageQueue, devToolsIntegration, }) => Effect.gen(function* () {
|
|
25
|
+
const { enqueueMessage, enqueueMessageEffect, drainPendingMessages, resetDrainBudget, } = messageQueue;
|
|
26
|
+
const { mountTracker, mountRuntime, drainMountEvents, readViewState, setViewState, isPausedNow, resumeDevTools, attachRenderedMounts, } = devToolsIntegration;
|
|
27
|
+
const settlePendingCommit = () => {
|
|
28
|
+
if (commitNotifier.service.isCommitPending()) {
|
|
29
|
+
commitNotifier.notifyCommitted();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
// NOTE: `isRenderFrameScheduled` clears when the frame callback
|
|
33
|
+
// starts, which on the View Transition path is before the patch runs.
|
|
34
|
+
// `commitNotifier` tracks the patch itself, so `Render.afterCommit`
|
|
35
|
+
// waits for the commit rather than for the frame that scheduled it.
|
|
36
|
+
let isRenderFrameScheduled = false;
|
|
37
|
+
// NOTE: resume clears the DevTools store's pause flag before its frame
|
|
38
|
+
// patches the live view. This distinguishes that intentional repaint
|
|
39
|
+
// from an ordinary frame that was already queued when jumpTo installed
|
|
40
|
+
// the historical view.
|
|
41
|
+
let isLiveViewRestorePending = false;
|
|
42
|
+
// NOTE: the Model behind the DOM currently on screen, which is what a
|
|
43
|
+
// View Transition animates away from. Seeded with `initModel` because
|
|
44
|
+
// the init render paints it, and advanced only where a render actually
|
|
45
|
+
// commits. The `viewTransition` predicate never runs before a Message
|
|
46
|
+
// has dirtied the Model, and the init render completes behind the boot
|
|
47
|
+
// barrier, so this is always the model of a paint that happened.
|
|
48
|
+
let lastRenderedModel = initModel;
|
|
49
|
+
// NOTE: the transition the browser is still animating, if any. Held so
|
|
50
|
+
// the runtime can skip it: when a later frame supersedes it, when the
|
|
51
|
+
// runtime crashes, and at teardown, where the browser would otherwise
|
|
52
|
+
// animate over a released container. Declared above `crashWith`, which
|
|
53
|
+
// calls the skip and can run as early as the init render.
|
|
54
|
+
let maybePendingViewTransition = Option.none();
|
|
55
|
+
const skipPendingViewTransition = () => {
|
|
56
|
+
if (Option.isSome(maybePendingViewTransition)) {
|
|
57
|
+
const { value: pendingViewTransition } = maybePendingViewTransition;
|
|
58
|
+
// NOTE: cleared first. An implementation that runs the update
|
|
59
|
+
// callback synchronously would otherwise re-enter this.
|
|
60
|
+
maybePendingViewTransition = Option.none();
|
|
61
|
+
pendingViewTransition.update.isInvalidated = true;
|
|
62
|
+
try {
|
|
63
|
+
pendingViewTransition.handle.skipTransition();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// NOTE: skipping runs on teardown and crash paths, so a refusal
|
|
67
|
+
// must not propagate into them.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// NOTE: the current vnode is plain closure state. The frame reads and
|
|
72
|
+
// writes it directly; the cold paths that run inside Effects (crash
|
|
73
|
+
// rendering, the dispose finalizer, the replay render) read the same
|
|
74
|
+
// slot synchronously, so no Ref is needed.
|
|
75
|
+
const vnodeSlot = { maybeCurrentVNode: Option.none() };
|
|
76
|
+
const patchRuntimeVNode = (maybeCurrentVNode, nextVNode, seen) => {
|
|
77
|
+
try {
|
|
78
|
+
return __patchVNode(maybeCurrentVNode, nextVNode, container, seen, patchedVNode => {
|
|
79
|
+
vnodeSlot.maybeCurrentVNode = Option.some(patchedVNode);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
try {
|
|
84
|
+
const maybeRecoveryVNode = vnodeSlot.maybeCurrentVNode;
|
|
85
|
+
if (Option.isSome(maybeRecoveryVNode)) {
|
|
86
|
+
vnodeSlot.maybeCurrentVNode = Option.some(__recoverVNodeAfterPatchFailure(maybeRecoveryVNode.value));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
flushReplayUnmountsAfterPatchFailure();
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
// NOTE: consumed by the first render only. Set when this boot found
|
|
96
|
+
// an adoptable server-rendered root; the first patch then goes
|
|
97
|
+
// through `__hydrateVNode` instead of replacing the container.
|
|
98
|
+
let pendingHydrationRoot = Option.getOrNull(maybeHydrationRoot);
|
|
99
|
+
// NOTE: registered before any perpetual fiber is forked so it runs
|
|
100
|
+
// after they are interrupted (scope finalizers are LIFO). Patching to
|
|
101
|
+
// an empty tree fires snabbdom destroy hooks, which is what releases
|
|
102
|
+
// Mounts; swapping the placeholder for the original container leaves
|
|
103
|
+
// the host DOM as it was before the first render, ready for a fresh
|
|
104
|
+
// embed of the same container. Gated on interruption: that is the
|
|
105
|
+
// dispose path. A runtime that stops because it crashed completes
|
|
106
|
+
// normally after rendering the crash view, and the crash view must
|
|
107
|
+
// stay visible.
|
|
108
|
+
yield* Effect.addFinalizer(exit => Effect.gen(function* () {
|
|
109
|
+
if (!Exit.hasInterrupts(exit)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const maybeCurrentVNode = vnodeSlot.maybeCurrentVNode;
|
|
113
|
+
yield* Option.match(maybeCurrentVNode, {
|
|
114
|
+
onNone: () => Effect.void,
|
|
115
|
+
onSome: currentVNode => Effect.sync(() => {
|
|
116
|
+
const placeholderNode = __patchVNode(Option.some(currentVNode), null, container).elm;
|
|
117
|
+
if (placeholderNode && placeholderNode.parentNode) {
|
|
118
|
+
placeholderNode.parentNode.replaceChild(container, placeholderNode);
|
|
119
|
+
container.replaceChildren();
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
}));
|
|
124
|
+
// NOTE: shared by every crash path: the init render, the plain
|
|
125
|
+
// message drain and render frame (which reach it through
|
|
126
|
+
// `Effect.runFork` from their catch blocks), and the Command and
|
|
127
|
+
// Subscription fibers (a Command's Effect and a Subscription's
|
|
128
|
+
// Stream are typed with a `never` error channel, so a cause
|
|
129
|
+
// escaping one can only be a `resources` Layer build failure or an
|
|
130
|
+
// escaped defect, both unrecoverable). Each path catches its own
|
|
131
|
+
// cause so a failure surfaces as the crash view instead of dying
|
|
132
|
+
// silently and leaving the DOM frozen at the last successful
|
|
133
|
+
// render. The first crash wins: concurrent Command fibers can fail
|
|
134
|
+
// on the same broken Layer, and only one should report and render.
|
|
135
|
+
const crashWith = (cause, maybeMessage) => Effect.sync(() => {
|
|
136
|
+
if (status.isCrashed) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
status.isCrashed = true;
|
|
140
|
+
// NOTE: the crash view should appear at once, not animate in from
|
|
141
|
+
// a snapshot of the state that crashed.
|
|
142
|
+
skipPendingViewTransition();
|
|
143
|
+
const model = readLiveModel();
|
|
144
|
+
const squashed = Cause.squash(cause);
|
|
145
|
+
const error = squashed instanceof Error ? squashed : new Error(String(squashed));
|
|
146
|
+
renderCrashView({ error, model, message: maybeMessage }, crash, container, vnodeSlot, manageDocument);
|
|
147
|
+
settlePendingCommit();
|
|
148
|
+
});
|
|
149
|
+
// NOTE: `maybeLastDirtyMessage` holds the most recent dirtying Message,
|
|
150
|
+
// so slow render-phase callbacks during high-rate bursts attribute to
|
|
151
|
+
// the last Message in the frame batch, not the specific one that pushed
|
|
152
|
+
// the view past threshold. Acceptable for a debug callback; full
|
|
153
|
+
// attribution would require correlating each message with its render
|
|
154
|
+
// contribution, which isn't worth the complexity.
|
|
155
|
+
let maybeLastDirtyMessage = Option.none();
|
|
156
|
+
const setLastDirtyMessage = (message) => {
|
|
157
|
+
maybeLastDirtyMessage = Option.some(message);
|
|
158
|
+
};
|
|
159
|
+
const dispatchSync = (message) => {
|
|
160
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
161
|
+
enqueueMessage(message);
|
|
162
|
+
};
|
|
163
|
+
const dispatchAsync = (message) =>
|
|
164
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
165
|
+
enqueueMessageEffect(message);
|
|
166
|
+
const dispatch = { dispatchAsync, dispatchSync };
|
|
167
|
+
// NOTE: One boundary registry per runtime instance, shared
|
|
168
|
+
// across renders so Submodel wrap descriptors registered by
|
|
169
|
+
// h.submodel persist between renders. The render function calls
|
|
170
|
+
// `beginHtmlRender` at the start of each pass; wraps for
|
|
171
|
+
// unmounted Submodels (e.g. an entry removed from a list) are
|
|
172
|
+
// dropped from the registry via snabbdom destroy hooks attached
|
|
173
|
+
// by `h.submodel` to each child vnode.
|
|
174
|
+
const boundaryRegistry = createHtmlBoundaryRegistry();
|
|
175
|
+
// NOTE: callers set `isRenderingFrame` before calling this and clear
|
|
176
|
+
// it after. Without it, a Message dispatched while the patch is still
|
|
177
|
+
// running would run update against a half-patched DOM. For a replay,
|
|
178
|
+
// `render` also calls `beginReplayHtmlRender` first, so the old
|
|
179
|
+
// tree's unmount callbacks don't dispatch into live history.
|
|
180
|
+
const renderSync = (model, maybeMessage, dispatchService, renderContext, renderMode) => {
|
|
181
|
+
const maybeLiveRender = Option.liftPredicate(renderMode, mode => mode === 'Live');
|
|
182
|
+
const maybeLiveSlowView = Option.flatMap(maybeLiveRender, () => maybeSlowView);
|
|
183
|
+
const maybeLiveSlowPatch = Option.flatMap(maybeLiveRender, () => maybeSlowPatch);
|
|
184
|
+
const [nextDocument, maybeViewDuration] = measureSlowPhase(maybeLiveSlowView, () => {
|
|
185
|
+
beginHtmlRender(boundaryRegistry);
|
|
186
|
+
setHtmlRuntime(dispatchService.dispatchSync, renderContext, boundaryRegistry, renderMode);
|
|
187
|
+
try {
|
|
188
|
+
return view(model, htmlBuilder);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
clearHtmlRuntime();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const { body: nextVNode } = nextDocument;
|
|
195
|
+
reportSlowPhase(maybeLiveSlowView, maybeViewDuration, (durationMs, thresholdMs) => ({
|
|
196
|
+
_tag: 'View',
|
|
197
|
+
model,
|
|
198
|
+
message: maybeMessage,
|
|
199
|
+
durationMs,
|
|
200
|
+
thresholdMs,
|
|
201
|
+
}));
|
|
202
|
+
const { maybeCurrentVNode } = vnodeSlot;
|
|
203
|
+
const [patchedVNode, maybePatchDuration] = measureSlowPhase(maybeLiveSlowPatch, () => {
|
|
204
|
+
if (Option.isNone(maybeCurrentVNode) &&
|
|
205
|
+
pendingHydrationRoot !== null) {
|
|
206
|
+
const hydrationRoot = pendingHydrationRoot;
|
|
207
|
+
pendingHydrationRoot = null;
|
|
208
|
+
// NOTE: strip the stamp before the patch, not after, so the
|
|
209
|
+
// patch is the sole owner of the root's attributes. It has
|
|
210
|
+
// already served its purpose of locating the root, and
|
|
211
|
+
// removing it after would delete a `data-foldkit-app` the view
|
|
212
|
+
// itself declares, which a later equal-vnode patch would not
|
|
213
|
+
// restore. Removing it here also stops a later boot on the same
|
|
214
|
+
// container (a dispose-then-embed remount) from re-detecting
|
|
215
|
+
// this now-consumed root as hydratable.
|
|
216
|
+
hydrationRoot.removeAttribute(FOLDKIT_APP_ATTRIBUTE);
|
|
217
|
+
// An empty id reaches the adoption step's own check as a
|
|
218
|
+
// value that matches nothing. Boot already refused a
|
|
219
|
+
// hydration without an id, so this stands in only for a
|
|
220
|
+
// caller that reached here another way.
|
|
221
|
+
return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen, buildId ?? '');
|
|
222
|
+
}
|
|
223
|
+
return patchRuntimeVNode(maybeCurrentVNode, nextVNode, boundaryRegistry.dedupeSeen);
|
|
224
|
+
});
|
|
225
|
+
vnodeSlot.maybeCurrentVNode = Option.some(patchedVNode);
|
|
226
|
+
reportSlowPhase(maybeLiveSlowPatch, maybePatchDuration, (durationMs, thresholdMs) => ({
|
|
227
|
+
_tag: 'Patch',
|
|
228
|
+
model,
|
|
229
|
+
message: maybeMessage,
|
|
230
|
+
durationMs,
|
|
231
|
+
thresholdMs,
|
|
232
|
+
}));
|
|
233
|
+
if (manageDocument) {
|
|
234
|
+
applyDocumentMetadata(nextDocument, patchedVNode.elm);
|
|
235
|
+
}
|
|
236
|
+
if (import.meta.hot) {
|
|
237
|
+
duplicateIdScanner?.schedule(patchedVNode.elm);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
// NOTE: `dispatchService` defaults to live dispatch but is overridable
|
|
241
|
+
// so a time-travel render can bind both declarative handlers and newly
|
|
242
|
+
// acquired Mounts to `noOpDispatch`. A live Mount keeps its dispatcher
|
|
243
|
+
// across replay, while a replay-created Mount stays muted until a live
|
|
244
|
+
// resume patch releases it and starts the live action. This preserves
|
|
245
|
+
// valid async results from live Mounts without granting a historical
|
|
246
|
+
// acquisition access to the live Model.
|
|
247
|
+
const render = (model, maybeMessage, dispatchService = dispatch, renderMode = 'Live') => Effect.gen(function* () {
|
|
248
|
+
status.isRenderingFrame = true;
|
|
249
|
+
const renderContext = yield* Effect.context();
|
|
250
|
+
if (renderMode === 'Replay') {
|
|
251
|
+
beginReplayHtmlRender();
|
|
252
|
+
}
|
|
253
|
+
renderSync(model, maybeMessage, dispatchService, renderContext, renderMode);
|
|
254
|
+
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
255
|
+
status.isRenderingFrame = false;
|
|
256
|
+
endReplayHtmlRender();
|
|
257
|
+
drainPendingMessages();
|
|
258
|
+
})), Effect.provideService(Dispatch, dispatchService), Effect.provideService(MountTracker, mountTracker), Effect.provideService(MountRuntime, mountRuntime));
|
|
259
|
+
const liveRenderContext = Context.add(Context.add(Context.add(runtimeContext, Dispatch, dispatch), MountTracker, mountTracker), MountRuntime, mountRuntime);
|
|
260
|
+
// NOTE: the render, Mount drain, DevTools attribution, and
|
|
261
|
+
// patch-time-buffer flush. Shared by the plain path (called directly)
|
|
262
|
+
// and the View Transition path (called from the transition's update
|
|
263
|
+
// callback), which run identical work; only whether they run inside
|
|
264
|
+
// `document.startViewTransition` differs. `isRenderingFrame` gates the
|
|
265
|
+
// buffering of Messages dispatched by patch-time hooks, so it must
|
|
266
|
+
// wrap the actual patch, which on the transition path happens inside
|
|
267
|
+
// the callback, not when the frame is scheduled.
|
|
268
|
+
const runRenderFrameBody = () => {
|
|
269
|
+
status.isRenderingFrame = true;
|
|
270
|
+
// NOTE: captured before the patch, because `drainPendingMessages`
|
|
271
|
+
// below can advance the live Model again before the next frame reads
|
|
272
|
+
// it. What this frame painted is what the next transition animates
|
|
273
|
+
// away from.
|
|
274
|
+
const renderedModel = readLiveModel();
|
|
275
|
+
const isResuming = readViewState() === 'Paused';
|
|
276
|
+
if (isResuming) {
|
|
277
|
+
// NOTE: Messages accumulated while a historical view owned the DOM
|
|
278
|
+
// did not cause this repaint. A Message arriving during the patch
|
|
279
|
+
// can repopulate this field when the buffered queue drains.
|
|
280
|
+
maybeLastDirtyMessage = Option.none();
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
renderSync(readLiveModel(), maybeLastDirtyMessage, dispatch, liveRenderContext, 'Live');
|
|
284
|
+
// NOTE: after the patch, so a render that threw leaves this on the
|
|
285
|
+
// Model still on screen, and before `drainPendingMessages` below,
|
|
286
|
+
// whose handlers can advance the live Model again.
|
|
287
|
+
lastRenderedModel = renderedModel;
|
|
288
|
+
// NOTE: resume clears the store's pause flag before this frame.
|
|
289
|
+
// Publish Live only after the live DOM has been installed.
|
|
290
|
+
setViewState('Live');
|
|
291
|
+
attachRenderedMounts();
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
Effect.runFork(crashWith(Cause.die(error), maybeLastDirtyMessage));
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
status.isRenderingFrame = false;
|
|
298
|
+
}
|
|
299
|
+
// NOTE: Messages dispatched by patch-time hooks (for example,
|
|
300
|
+
// OnUnmount destroys, or Mount emissions) were buffered while the
|
|
301
|
+
// frame held the stack; they process now, after the patch has
|
|
302
|
+
// committed and the frame's Mount events are attributed.
|
|
303
|
+
drainPendingMessages();
|
|
304
|
+
// NOTE: last, so a waiter resumed by the commit observes the same
|
|
305
|
+
// DOM and the same processed-Message ordering it saw when
|
|
306
|
+
// `afterCommit` counted frames.
|
|
307
|
+
settlePendingCommit();
|
|
308
|
+
};
|
|
309
|
+
// NOTE: starts a View Transition around this frame's render when the
|
|
310
|
+
// `viewTransition` predicate matches, returning `true` when it did.
|
|
311
|
+
// `startViewTransition` invokes its update callback asynchronously
|
|
312
|
+
// after snapshotting the old DOM, so the callback reads the live Model
|
|
313
|
+
// and `maybeLastDirtyMessage` fresh (the plain loop may have advanced
|
|
314
|
+
// the model while the browser suppressed rendering) and re-checks the
|
|
315
|
+
// disposal and crash guards, which can flip while the transition is
|
|
316
|
+
// pending. The unconfigured path never reaches this function; the
|
|
317
|
+
// `Option.isNone` check in `renderFramePlain` returns first, so a
|
|
318
|
+
// runtime without `viewTransition` allocates no per-frame callback.
|
|
319
|
+
const startFrameViewTransition = (resolved) => {
|
|
320
|
+
if (readViewState() === 'Paused') {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (resolved.reducedMotionQuery.matches) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (Option.isNone(maybeLastDirtyMessage)) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
const maybeDecision = __decideViewTransition(resolved.decide, {
|
|
330
|
+
previousModel: lastRenderedModel,
|
|
331
|
+
model: readLiveModel(),
|
|
332
|
+
message: maybeLastDirtyMessage.value,
|
|
333
|
+
});
|
|
334
|
+
if (Option.isNone(maybeDecision)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
// NOTE: invalidate an older transition before this one takes
|
|
338
|
+
// ownership of the latest live repaint. The browser may still call
|
|
339
|
+
// the older update callback, but its invalidation guard leaves the
|
|
340
|
+
// DOM and commit notifier to this transition.
|
|
341
|
+
skipPendingViewTransition();
|
|
342
|
+
try {
|
|
343
|
+
const update = {
|
|
344
|
+
isInvalidated: false,
|
|
345
|
+
didRun: false,
|
|
346
|
+
};
|
|
347
|
+
const handle = resolved.startViewTransition(() => {
|
|
348
|
+
if (update.isInvalidated || update.didRun) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
update.didRun = true;
|
|
352
|
+
// NOTE: the view state closes the resume window after the
|
|
353
|
+
// store is live but before its plain frame has restored the live
|
|
354
|
+
// DOM. A transition invalidated by jumpTo stays invalid forever,
|
|
355
|
+
// so its callback cannot repaint inside the stale transition
|
|
356
|
+
// even if it arrives after resume has published `Live`.
|
|
357
|
+
if (status.isRuntimeDisposed ||
|
|
358
|
+
status.isCrashed ||
|
|
359
|
+
isPausedNow() ||
|
|
360
|
+
readViewState() === 'Paused') {
|
|
361
|
+
settlePendingCommit();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
runRenderFrameBody();
|
|
365
|
+
}, maybeDecision.value.maybeTypes);
|
|
366
|
+
maybePendingViewTransition = Option.some({ handle, update });
|
|
367
|
+
__silenceViewTransitionRejections(handle);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// NOTE: an escaping throw would leave the rAF callback without a
|
|
372
|
+
// patch and without settling the commit notifier, parking every
|
|
373
|
+
// `Render.afterCommit` on this frame forever.
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
// NOTE: every path out of a scheduled frame settles the commit
|
|
378
|
+
// notifier, whether or not it patched. A frame abandoned silently
|
|
379
|
+
// would strand any `Render.afterCommit` registered against it, and
|
|
380
|
+
// the Dom helpers that gate on it would never run their DOM work.
|
|
381
|
+
const renderFramePlain = () => {
|
|
382
|
+
isRenderFrameScheduled = false;
|
|
383
|
+
const isRestoringLiveView = isLiveViewRestorePending;
|
|
384
|
+
isLiveViewRestorePending = false;
|
|
385
|
+
// NOTE: a frame scheduled before disposal fires after it; a
|
|
386
|
+
// disposed runtime must not repaint the released container.
|
|
387
|
+
if (status.isRuntimeDisposed) {
|
|
388
|
+
settlePendingCommit();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// NOTE: a frame is running, so the browser got control back; the
|
|
392
|
+
// drain budget starts fresh.
|
|
393
|
+
resetDrainBudget();
|
|
394
|
+
// NOTE: a Message that dirtied the model can also be the one
|
|
395
|
+
// whose Command crashed the runtime. Without this guard the
|
|
396
|
+
// next animation frame would render the live view over the
|
|
397
|
+
// crash view.
|
|
398
|
+
if (status.isCrashed) {
|
|
399
|
+
settlePendingCommit();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (isPausedNow()) {
|
|
403
|
+
settlePendingCommit();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (readViewState() === 'Paused' && !isRestoringLiveView) {
|
|
407
|
+
settlePendingCommit();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// NOTE: the unconfigured path pays one `Option.isNone` check and
|
|
411
|
+
// renders directly, allocating no per-frame callback. Only a
|
|
412
|
+
// runtime configured with `viewTransition` reaches
|
|
413
|
+
// `startFrameViewTransition`, which decides per frame whether to
|
|
414
|
+
// wrap the render in `document.startViewTransition`. When it does,
|
|
415
|
+
// the render runs later, inside the transition's update callback.
|
|
416
|
+
if (Option.isNone(maybeResolvedViewTransition)) {
|
|
417
|
+
runRenderFrameBody();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (!startFrameViewTransition(maybeResolvedViewTransition.value)) {
|
|
421
|
+
runRenderFrameBody();
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
// NOTE: render frames run as plain JavaScript inside the
|
|
425
|
+
// requestAnimationFrame callback. Messages arriving between frames mark
|
|
426
|
+
// at most one pending frame; the callback renders once with the latest
|
|
427
|
+
// model.
|
|
428
|
+
const scheduleRenderFrame = (isRestoringLiveView = false) => {
|
|
429
|
+
if (isRestoringLiveView) {
|
|
430
|
+
isLiveViewRestorePending = true;
|
|
431
|
+
}
|
|
432
|
+
if (isRenderFrameScheduled) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
isRenderFrameScheduled = true;
|
|
436
|
+
commitNotifier.markCommitPending();
|
|
437
|
+
requestAnimationFrame(renderFramePlain);
|
|
438
|
+
};
|
|
439
|
+
const devToolsRenderBridge = {
|
|
440
|
+
// NOTE: passes `noOpDispatch` so declarative handlers and Mounts
|
|
441
|
+
// acquired by the replay cannot reach the live Model. Their
|
|
442
|
+
// fibers stay alive and can observe view-state changes while the
|
|
443
|
+
// historical view owns them. If resume reuses such an element,
|
|
444
|
+
// OnMount releases the replay acquisition before starting the
|
|
445
|
+
// live action. Also discards mount events fired during the render
|
|
446
|
+
// so they don't get attributed to the next user-initiated dispatch.
|
|
447
|
+
renderReplay: model => Effect.gen(function* () {
|
|
448
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
449
|
+
const replayedModel = model;
|
|
450
|
+
const previousRenderedModel = lastRenderedModel;
|
|
451
|
+
// NOTE: a Mount surviving from the live view must observe
|
|
452
|
+
// Paused before the historical patch can expose different
|
|
453
|
+
// DOM. Mounts inserted by that patch capture this state when
|
|
454
|
+
// acquired, so asynchronous setup cannot skip Paused even if
|
|
455
|
+
// it consumes the Stream only after the live view returns.
|
|
456
|
+
setViewState('Paused');
|
|
457
|
+
// NOTE: a transition still animating belongs to the live
|
|
458
|
+
// state this replay is about to paint over. Left running it
|
|
459
|
+
// animates a dead snapshot across the replayed DOM.
|
|
460
|
+
skipPendingViewTransition();
|
|
461
|
+
const replayRenderExit = yield* Effect.exit(render(replayedModel, Option.none(), noOpDispatch, 'Replay'));
|
|
462
|
+
if (Exit.isFailure(replayRenderExit)) {
|
|
463
|
+
drainMountEvents();
|
|
464
|
+
if (isPausedNow()) {
|
|
465
|
+
// NOTE: the failed patch may already have changed the
|
|
466
|
+
// DOM. Repaint the Model at the store's previous paused
|
|
467
|
+
// index before returning the failure. If that Model no
|
|
468
|
+
// longer renders either, resume the store so its normal
|
|
469
|
+
// live frame becomes the single recovery path.
|
|
470
|
+
const rollbackExit = yield* Effect.exit(render(previousRenderedModel, Option.none(), noOpDispatch, 'Replay'));
|
|
471
|
+
drainMountEvents();
|
|
472
|
+
if (Exit.isFailure(rollbackExit)) {
|
|
473
|
+
yield* resumeDevTools;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
// NOTE: a failed first jump leaves the store live. Keep
|
|
478
|
+
// Mounts Paused through the recovery patch, which
|
|
479
|
+
// publishes Live only after the live DOM is restored.
|
|
480
|
+
yield* Effect.sync(() => scheduleRenderFrame(true));
|
|
481
|
+
}
|
|
482
|
+
return yield* Effect.failCause(replayRenderExit.cause);
|
|
483
|
+
}
|
|
484
|
+
drainMountEvents();
|
|
485
|
+
// NOTE: a replay paints a past Model, so it owns the DOM on
|
|
486
|
+
// screen until the next live frame. Leaving
|
|
487
|
+
// `lastRenderedModel` on the pre-pause Model would hand the
|
|
488
|
+
// `viewTransition` predicate a `previousModel` describing a
|
|
489
|
+
// DOM that no longer exists, and the frame `resume`
|
|
490
|
+
// schedules would animate the wrong direction out of the
|
|
491
|
+
// wrong snapshot.
|
|
492
|
+
lastRenderedModel = replayedModel;
|
|
493
|
+
// NOTE: the Message that dirtied the pre-pause frame does not
|
|
494
|
+
// describe this repaint. Clearing it means the frame `resume`
|
|
495
|
+
// schedules renders plainly, matching the documented rule
|
|
496
|
+
// that time-travel never animates.
|
|
497
|
+
maybeLastDirtyMessage = Option.none();
|
|
498
|
+
}),
|
|
499
|
+
// NOTE: `resume` calls this after a jumpTo render attached DOM
|
|
500
|
+
// listeners to `noOpDispatch`. Scheduling a frame renders the
|
|
501
|
+
// live model with live dispatch and rebinds listeners.
|
|
502
|
+
markRenderPending: Effect.sync(() => scheduleRenderFrame(true)),
|
|
503
|
+
};
|
|
504
|
+
return {
|
|
505
|
+
crashWith,
|
|
506
|
+
render,
|
|
507
|
+
setLastDirtyMessage,
|
|
508
|
+
scheduleRenderFrame,
|
|
509
|
+
skipPendingViewTransition,
|
|
510
|
+
devToolsRenderBridge,
|
|
511
|
+
};
|
|
512
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect, Layer, Option, Ref, type Scope } from 'effect';
|
|
2
|
+
import type { ManagedResourceConfig, ManagedResources } from '../managedResource/index.js';
|
|
3
|
+
import type { PortChannelsBundle } from './hostConnector.js';
|
|
4
|
+
/** A Managed Resource's config next to the Ref that holds its acquired value. */
|
|
5
|
+
export type ManagedResourceRef<Model, Message> = Readonly<{
|
|
6
|
+
config: ManagedResourceConfig<Model, Message>;
|
|
7
|
+
ref: Ref.Ref<Option.Option<unknown>>;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* The functions the runtime uses to give Commands and Subscriptions the
|
|
11
|
+
* app's `resources` Layer, its Managed Resources, its port channels, and
|
|
12
|
+
* the interrupt registry, and to give Flags the Layer alone, plus the
|
|
13
|
+
* Managed Resource refs the lifecycle fibers write to.
|
|
14
|
+
*/
|
|
15
|
+
export type ResourceProvider<Model, Message, Resources, ManagedResourceServices> = Readonly<{
|
|
16
|
+
managedResourceRefs: ReadonlyArray<ManagedResourceRef<Model, Message>>;
|
|
17
|
+
provideAllResources: <A>(effect: Effect.Effect<A, never, Resources | ManagedResourceServices>) => Effect.Effect<A>;
|
|
18
|
+
provideResources: <A>(effect: Effect.Effect<A, never, Resources>) => Effect.Effect<A>;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Builds the runtime's resource provider: one cached build of the
|
|
22
|
+
* `resources` Layer into the runtime scope, a Ref per Managed Resource, and
|
|
23
|
+
* the two functions that provide them. `provideAllResources` is for Commands
|
|
24
|
+
* and Subscriptions. `provideResources` is for Flags, which run before there
|
|
25
|
+
* is a Model to render a crash view against.
|
|
26
|
+
*/
|
|
27
|
+
export declare const makeResourceProvider: <Model, Message, Resources, ManagedResourceServices>({ resources, managedResources, runtimeScope, maybePortChannels, }: Readonly<{
|
|
28
|
+
resources: Layer.Layer<Resources> | undefined;
|
|
29
|
+
managedResources: ManagedResources<Model, Message, ManagedResourceServices> | undefined;
|
|
30
|
+
runtimeScope: Scope.Scope;
|
|
31
|
+
maybePortChannels: Option.Option<PortChannelsBundle>;
|
|
32
|
+
}>) => Effect.Effect<ResourceProvider<Model, Message, Resources, ManagedResourceServices>>;
|
|
33
|
+
//# sourceMappingURL=resourceProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resourceProvider.d.ts","sourceRoot":"","sources":["../../src/runtime/resourceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,MAAM,EACN,KAAK,EACL,MAAM,EAEN,GAAG,EACH,KAAK,KAAK,EACX,MAAM,QAAQ,CAAA;AAMf,OAAO,KAAK,EACV,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,6BAA6B,CAAA;AAEpC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,iFAAiF;AACjF,MAAM,MAAM,kBAAkB,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACxD,MAAM,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC7C,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;CACrC,CAAC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,CAC1B,KAAK,EACL,OAAO,EACP,SAAS,EACT,uBAAuB,IACrB,QAAQ,CAAC;IACX,mBAAmB,EAAE,aAAa,CAAC,kBAAkB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAA;IACtE,mBAAmB,EAAE,CAAC,CAAC,EACrB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,GAAG,uBAAuB,CAAC,KACjE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IACrB,gBAAgB,EAAE,CAAC,CAAC,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,KACvC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;CACtB,CAAC,CAAA;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC/B,KAAK,EACL,OAAO,EACP,SAAS,EACT,uBAAuB,qEAMtB,QAAQ,CAAC;IACV,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,SAAS,CAAA;IAC7C,gBAAgB,EACd,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,CAAC,GAAG,SAAS,CAAA;IACvE,YAAY,EAAE,KAAK,CAAC,KAAK,CAAA;IACzB,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;CACrD,CAAC,KAAG,MAAM,CAAC,MAAM,CAChB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,uBAAuB,CAAC,CAmKlE,CAAA"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Array, Cause, Context, Effect, Layer, Option, Record, Ref, } from 'effect';
|
|
2
|
+
import { __CurrentRegistry as __CurrentInterruptRegistry, __makeRegistry as __makeInterruptRegistry, } from '../command/interruptible/index.js';
|
|
3
|
+
import { __CurrentPortChannels } from '../port/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Builds the runtime's resource provider: one cached build of the
|
|
6
|
+
* `resources` Layer into the runtime scope, a Ref per Managed Resource, and
|
|
7
|
+
* the two functions that provide them. `provideAllResources` is for Commands
|
|
8
|
+
* and Subscriptions. `provideResources` is for Flags, which run before there
|
|
9
|
+
* is a Model to render a crash view against.
|
|
10
|
+
*/
|
|
11
|
+
export const makeResourceProvider = ({ resources, managedResources, runtimeScope, maybePortChannels, }) => Effect.gen(function* () {
|
|
12
|
+
// NOTE: `Effect.provide(effect, layer)` builds the Layer into a
|
|
13
|
+
// scope that closes when the provided effect ends, so providing the
|
|
14
|
+
// Layer per Command would construct and tear down every resource on
|
|
15
|
+
// each invocation. Building once into `runtimeScope` through a
|
|
16
|
+
// cached Effect is what makes `resources` long-lived: the first
|
|
17
|
+
// Command or Subscription that runs triggers construction, every
|
|
18
|
+
// later one shares the same built services, and release happens at
|
|
19
|
+
// runtime teardown. The build is uninterruptible because
|
|
20
|
+
// `Effect.cached` caches whatever Exit the first run produces:
|
|
21
|
+
// dispose racing an in-flight build would otherwise cache an
|
|
22
|
+
// interrupt, which every waiter would then surface as a crash.
|
|
23
|
+
const maybeAcquireResourceContext = yield* Option.match(Option.fromNullishOr(resources), {
|
|
24
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
25
|
+
onSome: resourceLayer => Effect.map(Effect.cached(Effect.uninterruptible(Layer.buildWithScope(resourceLayer, runtimeScope))), Option.some),
|
|
26
|
+
});
|
|
27
|
+
const managedResourceEntries = managedResources
|
|
28
|
+
? /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
29
|
+
Record.toEntries(managedResources)
|
|
30
|
+
: [];
|
|
31
|
+
const managedResourceRefs = yield* Effect.forEach(managedResourceEntries, ([_key, config]) => Ref.make(Option.none()).pipe(Effect.map(ref => ({ config, ref }))));
|
|
32
|
+
const mergeResourceIntoLayer = (layer, { config, ref }) => Layer.merge(layer, Layer.succeed(
|
|
33
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
34
|
+
config.resource._tag, ref));
|
|
35
|
+
const maybeManagedResourceLayer = Array.match(managedResourceRefs, {
|
|
36
|
+
onEmpty: () => Option.none(),
|
|
37
|
+
onNonEmpty: refs => Option.some(Array.reduce(refs,
|
|
38
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
39
|
+
Layer.empty, mergeResourceIntoLayer)),
|
|
40
|
+
});
|
|
41
|
+
const interruptRegistry = __makeInterruptRegistry();
|
|
42
|
+
const provideAllResources = (effect) => {
|
|
43
|
+
const withResources = Option.match(maybeAcquireResourceContext, {
|
|
44
|
+
onNone: () => effect,
|
|
45
|
+
onSome: acquireResourceContext => Effect.flatMap(acquireResourceContext, resourceContext => Effect.provideContext(effect, resourceContext)),
|
|
46
|
+
});
|
|
47
|
+
const withManagedResources = Option.match(maybeManagedResourceLayer, {
|
|
48
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
49
|
+
onNone: () => withResources,
|
|
50
|
+
onSome: managedLayer =>
|
|
51
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
52
|
+
Effect.provide(withResources, managedLayer),
|
|
53
|
+
});
|
|
54
|
+
const withPortChannels = Option.match(maybePortChannels, {
|
|
55
|
+
onNone: () => withManagedResources,
|
|
56
|
+
onSome: portChannels => Effect.provideService(withManagedResources, __CurrentPortChannels, portChannels.channels),
|
|
57
|
+
});
|
|
58
|
+
return Effect.provideService(withPortChannels, __CurrentInterruptRegistry, interruptRegistry);
|
|
59
|
+
};
|
|
60
|
+
// NOTE: Flags run through the same cached build that Commands and
|
|
61
|
+
// Subscriptions use, rather than being handed the Layer again, so a
|
|
62
|
+
// service needed both at startup and by a Command is constructed
|
|
63
|
+
// once. An app without Flags never reaches it, which keeps the Layer
|
|
64
|
+
// lazy when the first thing that needs it is a Command.
|
|
65
|
+
//
|
|
66
|
+
// NOTE: a Layer that fails to build is not fatal here. Flags resolve
|
|
67
|
+
// before `init`, so there is no Model for a crash view to render
|
|
68
|
+
// against and a failure escaping this point kills the app with a
|
|
69
|
+
// blank container. Running Flags against an empty context instead
|
|
70
|
+
// lets an app whose Flags never touch the Layer boot as it did
|
|
71
|
+
// before Flags could consume `resources`: the cached failure then
|
|
72
|
+
// surfaces at the first Command or Subscription, where `crashWith`
|
|
73
|
+
// does render the crash view. Flags that do need the Layer still
|
|
74
|
+
// fail here, and both causes are reported: the `Service not found`
|
|
75
|
+
// defect the empty context produced is useless on its own, and the
|
|
76
|
+
// build failure that explains it would be lost if it replaced the
|
|
77
|
+
// Flags cause outright. Combining them also keeps a Flags Effect
|
|
78
|
+
// that fails for its own unrelated reason visible instead of
|
|
79
|
+
// attributing its defect to the Layer. Interrupts propagate
|
|
80
|
+
// untouched on both sides, because dispose racing either the build
|
|
81
|
+
// or the Flags run is not a failure to recover from, and
|
|
82
|
+
// `Effect.catchCause` hands the handler interrupt causes too.
|
|
83
|
+
const provideResources = (effect) => Option.match(maybeAcquireResourceContext, {
|
|
84
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
85
|
+
onNone: () => effect,
|
|
86
|
+
onSome: acquireResourceContext => Effect.matchCauseEffect(acquireResourceContext, {
|
|
87
|
+
onFailure: buildCause => Cause.hasInterruptsOnly(buildCause)
|
|
88
|
+
? Effect.failCause(buildCause)
|
|
89
|
+
: Effect.catchCause(Effect.provideContext(effect,
|
|
90
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
91
|
+
Context.empty()), flagsCause => Cause.hasInterruptsOnly(flagsCause)
|
|
92
|
+
? Effect.failCause(flagsCause)
|
|
93
|
+
: Effect.failCause(Cause.combine(buildCause, flagsCause))),
|
|
94
|
+
onSuccess: resourceContext => Effect.provideContext(effect, resourceContext),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
return { managedResourceRefs, provideAllResources, provideResources };
|
|
98
|
+
});
|