@statekit/machine-message
v0.3.2-dev
Published
Maintainers
Readme
@statekit/machine-message
A request cache built on a StateKit machine. Use it when several places need the same request result.
Easy example
import { getMessage } from "./messages"
type User = {
id: number
name: string
}
const getUser = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
const message = getMessage(getUser, [42])
message.isLoading // true
const user = await message.promise
user.name // "Ada"
message.data?.name // "Ada"That is all a new request needs. The function and arguments identify its cached value.
Install
pnpm add @statekit/machine-message @statekit/react reactWork in progress. The request cache works. Live channel delivery is not ready yet.
Set up once
Create one message machine for the app:
import { createMessageMachine } from "@statekit/machine-message"
export const { messageMachine, getMessage } = createMessageMachine({
staleTime: 30_000,
})Use its hook once near the root of the React app:
import { createMachineContext } from "@statekit/react"
import { messageMachine } from "./messages"
const [useMessageMachine] = createMachineContext(messageMachine)
export const App = () => {
useMessageMachine()
return <YourApp />
}The first render in the browser initializes the cache. After that, every component, event, timer, and normal function that uses this getMessage shares it.
getMessage starts the request when there is no cached value, when the value is stale, after a failed request, or when force is true. While a request is in flight, later calls share it instead of sending another.
Cache keys
The function and its arguments form the cache key:
getMessage(getUser, [42])
getMessage(getUser, [7]) // different user, different entryReuse the same function object. If that is not possible, give it a stable string or symbol id:
getMessage(["user", getUser], [42])Arguments are part of the key. Object keys are sorted, so these calls share one entry:
getMessage(search, [{ page: 1, text: "statekit" }])
getMessage(search, [{ text: "statekit", page: 1 }])A Date argument is keyed by its time.
Cached and stale data
await getMessage(["user", getUser], [42]).promise
await getMessage(["user", getUser], [42]).promise
// Uses the cached value while it is fresh.
await getMessage(["user", getUser], [42], { force: true }).promise
// Runs getUser again.staleTime is set when the message machine is created. The default is five seconds; staleTime: undefined never goes stale. A stale entry is fetched again on the next getMessage call.
To give one entry its own stale time, pass staleTime to getMessage. It is saved on the entry when a request is sent:
getMessage(["user", getUser], [42], { staleTime: 60_000 })If a refresh fails, the public promise resolves to the previous cached value. It resolves to undefined when there was no previous value. It does not reject.
Read request state
Each getMessage call returns a snapshot of the request flags:
const pending = getMessage(["user", getUser], [42])
pending.isLoading // true
await pending.promise
const settled = getMessage(["user", getUser], [42])
settled.isSuccess // true
settled.isStale // false while fresh
settled.data // latest cached value
settled.error // latest error, if presentThe flags are isInitial, isLoading, isSuccess, isFailed, isStale, isOpen, and isClosed. Call getMessage again when you need fresh flags.
React updates
getMessage does not subscribe a component to later request changes. Await its promise or keep the result in local state when the screen must update.
Channel API
openMessageChannel and closeMessageChannel set the open or closed flag on a successful request and clear the other one.
onChannelMessage is not ready for live messages. It stores listeners, but the current package has no code that sends a message to them. Do not use it for WebSocket or stream delivery yet.
Do
- Use
messageMachine's hook once near the root before the first request. - Reuse request functions or give them an explicit id.
- Pass arguments as a tuple.
- Call
getMessageagain to read fresh request flags. - Use
force: trueonly when the cache must be skipped.
Don't
- Don't expect the public request promise to reject.
- Don't use changing inline functions as cache keys.
- Don't pass cyclic objects as arguments.
- Don't mutate
messageMachine.data. - Don't call
getMessagewhile server rendering; the cache is only initialized in the browser. - Don't rely on
onChannelMessagefor message delivery yet.
How it works
component, event, or normal function
|
v
getMessage(getUser, [42])
|
v
key: getUser + [42]
|
+--------+---------+
| |
fresh cache missing / stale / force
| |
v v
return cached data run getUser(42)
|
v
cache the resultThe same call always points to the same cache entry:
missing -> loading -> success -> fresh
| |
| +-- time passes --> stale on next read
|
+-- failed --> keep the previous data, if there is anyDevelopment
From the node directory:
pnpm --filter @statekit/machine-message test -- --runInBand
pnpm --filter @statekit/machine-message build