On this pageOverview
Managed Resources
Resources live for the entire application lifecycle. But some resources are heavy and should only be active while the model is in a particular state, like a camera stream during a video call, a WebSocket connection while on a chat page, or a Web Worker pool during a computation. Managed resources provide model-driven acquire/release lifecycle, using the same deps-diffing engine as subscriptions.
The restaurant analogy
If resources are kitchen equipment (permanent, always on), managed resources are specialty ingredients sourced on demand. When the menu shifts to a seafood special (model state changes), the kitchen orders in fresh lobster and sets up the shellfish station. When the special ends, the lobster goes back to the supplier and the station is broken down. If the chef (Command) tries to plate lobster when it’s not in season, they get a clear signal: ResourceNotAvailable. And if the special changes from Maine lobster to king crab (params change), the old stock is returned and new stock is sourced, just like switching camera resolutions triggers release and reacquire.
Define a managed resource identity with ManagedResource.tag, then wire its lifecycle with makeManagedResources. The modelToMaybeRequirements function returns Option.some(params) when the resource should be active, and Option.none() when it should be released.
import { Effect, Option, Schema as S, pipe } from 'effect'
import { ManagedResource, Runtime } from 'foldkit'
// 1. Define a managed resource identity
const CameraStream = ManagedResource.tag<MediaStream>()('CameraStream')
// 2. Define a requirements schema. Option.some = active, Option.none = inactive
const ManagedResourceDeps = S.Struct({
camera: S.Option(S.Struct({ facingMode: S.String })),
})
// 3. Wire lifecycle with makeManagedResources
const managedResources = ManagedResource.makeManagedResources(
ManagedResourceDeps,
)<Model, Message>({
camera: {
resource: CameraStream,
modelToMaybeRequirements: model =>
pipe(
model.callState,
Option.liftPredicate(
(callState): callState is typeof InCall.Type =>
callState._tag === 'InCall',
),
Option.map(callState => ({
facingMode: callState.facingMode,
})),
),
acquire: ({ facingMode }) =>
Effect.tryPromise(() =>
navigator.mediaDevices.getUserMedia({
video: { facingMode },
}),
),
release: stream =>
Effect.sync(() => stream.getTracks().forEach(track => track.stop())),
onAcquired: () => AcquiredCamera(),
onReleased: () => ReleasedCamera(),
onAcquireError: error => FailedAcquireCamera({ error: String(error) }),
},
})
// 4. Pass to makeProgram
const program = Runtime.makeProgram({
Model,
init,
update,
view,
container: document.getElementById('root')!,
managedResources,
})When requirements change, the runtime handles the lifecycle automatically. If modelToMaybeRequirements transitions from Option.none() to Option.some(params), the resource is acquired and onAcquired is sent. When it goes back to Option.none(), the resource is released and onReleased is sent. If the params change while active (e.g. switching cameras), the old resource is released and a new one is acquired with the new params.
If acquisition fails, onAcquireError is sent as a message. The resource daemon continues watching for the next deps change. A failed acquisition does not crash the application.
Commands access the resource value via .get. Since the resource might not be active, .get can fail with ResourceNotAvailable. The type system enforces this: your Command won’t compile unless you handle the error.
import { Array, Effect, Option } from 'effect'
import { Command, ManagedResource } from 'foldkit'
const CameraStream = ManagedResource.tag<MediaStream>()('CameraStream')
const TakePhoto = Command.define(
'TakePhoto',
SucceededTakePhoto,
CameraUnavailable,
)
const takePhoto = () =>
TakePhoto(
Effect.gen(function* () {
const stream = yield* CameraStream.get
const maybeTrack = Array.head(stream.getVideoTracks())
const bitmap = yield* Option.match(maybeTrack, {
onNone: () => Effect.fail(new Error('No video track available')),
onSome: track => {
const imageCapture = new ImageCapture(track)
return Effect.promise(() => imageCapture.grabFrame())
},
})
return SucceededTakePhoto({ width: bitmap.width, height: bitmap.height })
}).pipe(
Effect.catchTag('ResourceNotAvailable', () =>
Effect.succeed(CameraUnavailable()),
),
),
)This is the same catchTag pattern you already use for Command errors. If your model correctly gates Commands (only dispatching takePhoto after AcquiredCamera has been received), the catchTag is a safety net that never fires. But if your model logic has a bug, you get a graceful error message instead of a crash.
Resources vs Managed Resources
Use resources for things that live forever (AudioContext, CanvasRenderingContext2D). Use managedResources for things tied to a model state (camera streams, WebSocket connections, media recorders).
With resources and managed resources, your app can work with any browser API. But what happens when something goes seriously wrong, like an unrecoverable error in update, view, or a Command? The next page covers crash views.