- TypeScript 98%
- Python 2%
prefect-push #
tells Prefect for iOS when a
flow run starts waiting on a person. it watches one Prefect server's
/api/events/out socket and, when a run pauses or suspends, sends each
registered phone an APNs alert (with Resume and Cancel actions) and starts a
"waiting on you" live activity. when the run moves on, it ends the activity.
bun and typescript, no runtime dependencies: bun:sqlite for the registry,
node:http2 and node:crypto for APNs token auth. expo's push service can't
start live activities, so this talks to APNs directly.
how it behaves #
- one relay watches one server. it signs in with that server's read-only credential and never changes anything on it.
- registering a device is authorized by the Prefect server: the relay
forwards the caller's
Authorizationheader toPOST /flows/countand accepts whoever the server accepts (cached for five minutes). - a pause is announced once. pauses are keyed by their state id, so an event and a listing, or a reconnect, don't double-send.
- nothing is missed while disconnected. the socket reconnects with backoff, and on every subscribe (and each minute) the relay lists what is paused: anything new and under 15 minutes old is announced, anything that stopped waiting has its activity ended. on a first boot this means old pauses are recorded quietly rather than announced.
- a pause about to time out is announced once more. with five minutes
left, a pause that was given at least ten gets one more alert ("Times out
in 4 min. …"), from the server's own
pause_timeout. a suspend without a timeout never does. - a device on the wrong APNs host is corrected, not dropped. the wrong
host answers
BadDeviceTokenjust as it does for a dead token, and a TestFlight build cannot always tell which environment its tokens belong to. the other host is tried first and the device's environment updated. - dead tokens are dropped: a 410,
BadDeviceTokenfrom both hosts,UnregisteredorDeviceTokenNotForTopicremoves the device (or just its push-to-start token). - without an APNs key it runs and says so in
/health("apns": false); nothing is sent.
api #
all under BASE_PATH (/push in production). everything but /health needs
the Prefect server's credential.
GET /health |
{ ok, subscribed, apns, devices, waiting } |
POST /devices |
{ id, token, start_token?, environment, server_id }: register or refresh. environment is the app's aps-environment; server_id is echoed in every push so the app knows which saved server a run belongs to |
DELETE /devices/{id} |
unregister |
PUT /devices/{id}/activities/{run_id} |
{ token }: the push token of a run's live activity, used to end it |
POST /devices/{id}/test |
send a test alert |
what it sends #
- alert: says what the run made, then what it asks, read as the app
reads it (
src/context.ts, in step with prefect-iossrc/waiting.ts). the context is the run'swork.waitingbody, else the markdown description stored with its input, else its approval artifact, else the newest event naming the run, from before the pause, with a stringpayload.body. the title iswork.waiting's title, else the artifact's description, else the body's opening heading or bold line, else the input's one question, else aprompt-like parameter, else "Suspended until you resume it"; the subtitle is the flow; the body is the context's list items joined (or its opening text) and then the question. categorywaiting-replywhen the pause asks for one line of text (the app offers Reply),waiting-inputfor any other input, elsewaiting.body: { runId, serverId, url }is what expo-notifications hands the app as the notification's data. - live activity start (
apns-push-type: liveactivity, topic<bundle>.push-type.liveactivity):attributes-typeLiveActivityAttributesand content state{ name: "WaitingRun", props }, the shapes expo-widgets declares.WaitingPropsinsrc/payload.tsmust stay in step with the app's. - live activity end: the same props with
endedset to the new state's name, dismissed a minute later.
run it #
bun install
bun run check
PREFECT_API_URL=https://prefect.example.com/api PREFECT_API_AUTH_STRING=user:pass bun src/main.ts
| env | |
|---|---|
PREFECT_API_URL |
API root of the server to watch (required) |
PREFECT_API_AUTH_STRING |
user:pass; read-only is enough. unset for an open server |
APNS_KEY or APNS_KEY_PATH |
the .p8 token signing key, inline or as a file |
APNS_KEY_ID, APNS_TEAM_ID |
the key's id and the Apple team's |
APNS_TOPIC |
the app's bundle id (default io.zzstoatzz.prefect) |
DB_PATH |
sqlite file (default prefect-push.db) |
PORT, BASE_PATH |
default 8080 and none |
deploy #
it runs in the prefect namespace beside prefect-server, from a ConfigMap of
src/ on the stock bun image, at https://prefect-server.waow.tech/push.
just deploy # check, ship the committed source, wait for /health
just apns-key ~/Downloads/AuthKey_XXXXXXXXXX.p8 XXXXXXXXXX TEAMID
just status
just logs -f