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 — 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.
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 =>
FailedToAcquireCamera({ error: String(error) }),
},
})
// 4. Pass to makeElement or makeApplication
const element = Runtime.makeElement({
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 { ManagedResource } from 'foldkit'
import { Command } from 'foldkit/command'
const CameraStream =
ManagedResource.tag<MediaStream>()('CameraStream')
// .get carries the resource identity in the R channel,
// so TypeScript verifies the resource is registered at compile time
const takePhoto = (): Command<
typeof TookPhoto | typeof CameraUnavailable,
never,
ManagedResource.ServiceOf<typeof CameraStream>
> =>
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 TookPhoto({ 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).