@llui/effects
Effect builders for LLui. Effects are data -- update() returns them, the runtime dispatches.
pnpm add @llui/effects
Usage
import { http, cancel, debounce, handleEffects } from '@llui/effects'
// Debounced search with cancel
function update(state: State, msg: Msg): [State, Effect[]] {
switch (msg.type) {
case 'search':
return [
{ ...state, query: msg.value },
[
cancel('search'),
debounce(
'search',
300,
http({
url: `/api/search?q=${msg.value}`,
onSuccess: (data) => ({ type: 'results', data }),
onError: (err) => ({ type: 'searchError', err }),
}),
),
],
]
}
}
// Wire up in component
const handler = handleEffects<Effect, Msg>()
.use(httpPlugin)
.else((effect, send) => {
/* custom effects */
})
API
Effect Builders
| Function | Description |
|---|---|
http({ url, onSuccess, onError }) |
HTTP request effect |
cancel(token, inner?) |
Cancel by token, optionally replace with inner |
debounce(key, ms, inner) |
Debounce inner effect by key |
timeout(ms, msg) |
Fire msg after delay |
interval(ms, msg) |
Fire msg on interval |
storageSet(key, value, storage?) |
Write to localStorage/sessionStorage |
storageGet(key, onResult, storage?) |
Read from storage |
storageRemove(key, storage?) |
Remove from storage |
storageWatch(key, onChange) |
Watch storage for changes |
broadcast(channel, data) |
Send on BroadcastChannel |
broadcastListen(channel, onMsg) |
Listen on BroadcastChannel |
sequence([...effects]) |
Run effects in order |
race([...effects]) |
Run effects concurrently, first wins |
upload({ url, body, onProgress, onSuccess, onError }) |
File upload with progress via XHR |
clipboardRead({ onSuccess, onError }) |
Read text from clipboard |
clipboardWrite(text) |
Write text to clipboard (fire-and-forget) |
notification(title, opts?) |
Show browser notification (requests permission) |
geolocation({ onSuccess, onError, enableHighAccuracy? }) |
One-shot geolocation position |
Upload
Upload files with progress tracking via XMLHttpRequest:
import { upload } from '@llui/effects'
const effect = upload({
url: '/api/upload',
body: formData,
headers: { Authorization: `Bearer ${token}` },
onProgress: (loaded, total) => ({
type: 'uploadProgress',
pct: Math.round((loaded / total) * 100),
}),
onSuccess: (data, status) => ({ type: 'uploadDone', data, status }),
onError: (error) => ({ type: 'uploadFailed', error }),
})
Clipboard
Read and write text via the Clipboard API:
import { clipboardRead, clipboardWrite } from '@llui/effects'
// Copy text to clipboard (fire-and-forget)
clipboardWrite('Hello, world!')
// Read text from clipboard
clipboardRead({
onSuccess: (text) => ({ type: 'pasted', text }),
onError: (error) => ({ type: 'clipError', error }),
})
Notification
Show browser notifications (requests permission automatically):
import { notification } from '@llui/effects'
notification('New message', {
body: 'You have a new message from Alice',
icon: '/avatar.png',
onClick: () => ({ type: 'openChat' }),
onError: () => ({ type: 'notifBlocked' }),
})
Geolocation
One-shot position request:
import { geolocation } from '@llui/effects'
geolocation({
enableHighAccuracy: true,
onSuccess: (pos) => ({
type: 'located',
lat: pos.latitude,
lng: pos.longitude,
}),
onError: (error) => ({ type: 'geoError', error }),
})
Effect Handling
| Function | Description |
|---|---|
handleEffects<E, M>() |
Chainable effect handler builder |
.use(plugin) |
Add an effect handler plugin |
.else(handler) |
Fallback for unhandled effects |
resolveEffects(def) |
SSR data loading -- resolves effects server-side |
Types
| Type | Description |
|---|---|
Async<T, E> |
idle | loading | success | failure -- async data state |
ApiError |
network | timeout | notfound | unauthorized | forbidden | ratelimit | validation | server |
Functions
http()
function http<M>(opts: {
url: string
method?: string
body?: unknown
contentType?: string
headers?: Record<string, string>
timeout?: number
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer'
onSuccess: (data: unknown, headers: Headers) => M
onError: (error: ApiError) => M
}): HttpEffect<M>
cancel()
export function cancel(token: string): CancelEffect
export function cancel(token: string, inner: BuiltinEffect): CancelReplaceEffect
debounce()
function debounce(key: string, ms: number, inner: BuiltinEffect): DebounceEffect
timeout()
function timeout<M>(ms: number, msg: M): TimeoutEffect
interval()
function interval<M>(key: string, ms: number, msg: M): IntervalEffect
storageLoad()
Synchronous read from storage. Use at init time to seed state. Returns null on miss or invalid JSON.
function storageLoad<T = unknown>(key: string, scope: StorageScope = 'local'): T | null
storageSet()
function storageSet(key: string, value: unknown, scope: StorageScope = 'local'): StorageSetEffect
storageRemove()
function storageRemove(key: string, scope: StorageScope = 'local'): StorageRemoveEffect
storageGet()
function storageGet<M>(key: string, onLoad: (value: unknown) => M, scope: StorageScope = 'local'): StorageGetEffect<M>
storageWatch()
function storageWatch<M>(key: string, onChange: (value: unknown) => M, scope: StorageScope = 'local'): StorageWatchEffect<M>
broadcast()
function broadcast(channel: string, data: unknown): BroadcastEffect
broadcastListen()
function broadcastListen<M>(channel: string, onMessage: (data: unknown) => M): BroadcastListenEffect<M>
websocket()
function websocket<M>(opts: {
url: string
key: string
protocols?: string[]
onOpen?: () => M
onMessage: (data: unknown) => M
onClose?: (code: number, reason: string) => M
onError?: () => M
}): WebSocketEffect<M>
wsSend()
function wsSend(key: string, data: unknown): WebSocketSendEffect
retry()
function retry(inner: BuiltinEffect, opts: { maxAttempts: number; delayMs: number }): RetryEffect
upload()
function upload<M>(opts: {
url: string
method?: string
body: FormData | Blob
headers?: Record<string, string>
onProgress: (loaded: number, total: number) => M
onSuccess: (data: unknown, status: number) => M
onError: (error: ApiError) => M
}): UploadEffect<M>
clipboardRead()
function clipboardRead<M>(opts: {
onSuccess: (text: string) => M
onError: (error: string) => M
}): ClipboardReadEffect<M>
clipboardWrite()
function clipboardWrite(text: string): ClipboardWriteEffect
notification()
function notification<M>(title: string, opts?: {
body?: string
icon?: string
tag?: string
onClick?: () => M
onClose?: () => M
onError?: () => M
}): NotificationEffect<M>
geolocation()
function geolocation<M>(opts: {
onSuccess: (position: { latitude: number; longitude: number; accuracy: number }) => M
onError: (error: string) => M
enableHighAccuracy?: boolean
}): GeolocationEffect<M>
sequence()
function sequence(effects: BuiltinEffect[]): SequenceEffect
race()
function race(effects: BuiltinEffect[]): RaceEffect
handleEffects()
function handleEffects<E extends { type: string }, M = never>(): EffectChain<E, M>
Types
Async
Models the lifecycle of an async operation.
export type Async<T, E> =
| { type: 'idle' }
| { type: 'loading'; stale?: T }
| { type: 'success'; data: T }
| { type: 'failure'; error: E }
ApiError
Standard API error type produced by the http() effect.
export type ApiError =
| { kind: 'network'; message: string }
| { kind: 'timeout' }
| { kind: 'notfound' }
| { kind: 'unauthorized' }
| { kind: 'forbidden' }
| { kind: 'ratelimit'; retryAfter?: number }
| { kind: 'validation'; fields: Record<string, string[]> }
| { kind: 'server'; status: number; message: string }
StorageScope
export type StorageScope = 'local' | 'session'
EffectPlugin
Plugin handler — returns true if the effect was handled, false to pass through.
export type EffectPlugin<E, M> = (ctx: EffectCtx<E, M>) => boolean
Interfaces
HttpEffect
export interface HttpEffect<M = unknown> {
type: 'http'
url: string
method?: string
body?: unknown
contentType?: string
headers?: Record<string, string>
timeout?: number
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer'
onSuccess: (data: unknown, headers: Headers) => M
onError: (error: ApiError) => M
}
CancelEffect
export interface CancelEffect {
type: 'cancel'
token: string
}
CancelReplaceEffect
export interface CancelReplaceEffect {
type: 'cancel'
token: string
inner: BuiltinEffect
}
DebounceEffect
export interface DebounceEffect {
type: 'debounce'
key: string
ms: number
inner: BuiltinEffect
}
TimeoutEffect
Fires msg once, after ms milliseconds. Auto-cancels if the component unmounts.
export interface TimeoutEffect {
type: 'timeout'
ms: number
msg: unknown
}
IntervalEffect
Fires msg every ms milliseconds. Cancel with cancel(key).
export interface IntervalEffect {
type: 'interval'
key: string
ms: number
msg: unknown
}
StorageSetEffect
Write a JSON value to localStorage/sessionStorage. Fire-and-forget.
export interface StorageSetEffect {
type: 'storage-set'
key: string
value: unknown
scope: StorageScope
}
StorageRemoveEffect
Remove a key from storage. Fire-and-forget.
export interface StorageRemoveEffect {
type: 'storage-remove'
key: string
scope: StorageScope
}
StorageGetEffect
Read a key from storage, dispatch the message returned by onLoad(value).
export interface StorageGetEffect<M = unknown> {
type: 'storage-get'
key: string
onLoad: (value: unknown) => M
scope: StorageScope
}
StorageWatchEffect
Listen for changes to a storage key. Fires the message returned by onChange(value) on cross-tab writes.
export interface StorageWatchEffect<M = unknown> {
type: 'storage-watch'
key: string
onChange: (value: unknown) => M
scope: StorageScope
}
BroadcastEffect
Post a message to a BroadcastChannel. Fire-and-forget.
export interface BroadcastEffect {
type: 'broadcast'
channel: string
data: unknown
}
BroadcastListenEffect
Subscribe to a BroadcastChannel. Fires the message returned by onMessage(data) per incoming message.
export interface BroadcastListenEffect<M = unknown> {
type: 'broadcast-listen'
channel: string
onMessage: (data: unknown) => M
}
SequenceEffect
export interface SequenceEffect {
type: 'sequence'
effects: BuiltinEffect[]
}
RaceEffect
export interface RaceEffect {
type: 'race'
effects: BuiltinEffect[]
}
WebSocketEffect
export interface WebSocketEffect<M = unknown> {
type: 'websocket'
url: string
key: string
protocols?: string[]
onOpen?: () => M
onMessage: (data: unknown) => M
onClose?: (code: number, reason: string) => M
onError?: () => M
}
WebSocketSendEffect
export interface WebSocketSendEffect {
type: 'ws-send'
key: string
data: unknown
}
UploadEffect
export interface UploadEffect<M = unknown> {
type: 'upload'
url: string
method?: string
body: FormData | Blob
headers?: Record<string, string>
onProgress: (loaded: number, total: number) => M
onSuccess: (data: unknown, status: number) => M
onError: (error: ApiError) => M
}
RetryEffect
export interface RetryEffect {
type: 'retry'
inner: BuiltinEffect
maxAttempts: number
delayMs: number
}
ClipboardReadEffect
export interface ClipboardReadEffect<M = unknown> {
type: 'clipboard-read'
onSuccess: (text: string) => M
onError: (error: string) => M
}
ClipboardWriteEffect
export interface ClipboardWriteEffect {
type: 'clipboard-write'
text: string
}
NotificationEffect
export interface NotificationEffect<M = unknown> {
type: 'notification'
title: string
body?: string
icon?: string
tag?: string
onClick?: () => M
onClose?: () => M
onError?: () => M
}
GeolocationEffect
export interface GeolocationEffect<M = unknown> {
type: 'geolocation'
onSuccess: (position: { latitude: number; longitude: number; accuracy: number }) => M
onError: (error: string) => M
enableHighAccuracy?: boolean
}
EffectCtx
export interface EffectCtx<E, M> {
effect: E
send: (msg: M) => void
signal: AbortSignal
}