Skip to content

Commit

Permalink
feat: image handler previous state (#427)
Browse files Browse the repository at this point in the history
* feat: provide `previousState` to image handler

* docs: up

* chore: changesets

* docs: add a little hint on how to access previous state
  • Loading branch information
dalechyn authored Jul 22, 2024
1 parent bab03ac commit ae57791
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-pianos-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"frog": patch
---

Added access to `previousState` and `previousButtonValues` in Image Handler.
10 changes: 8 additions & 2 deletions playground/src/initial.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Button, Frog } from 'frog'
import { Heading, VStack, vars } from './ui.js'

export const app = new Frog({
export const app = new Frog<{ State: { counter: number } }>({
ui: { vars },
initialState: { counter: 0 },
verify: false,
title: 'Initial',
})
.frame('/', (c) => {
c.deriveState((prev) => {
prev.counter++
})
return c.res({
image: '/refreshing-image/cool-parameter',
intents: [<Button>Check again</Button>],
Expand All @@ -21,7 +26,8 @@ export const app = new Frog({
image: (
<VStack grow gap="4">
<Heading color="text400">
Current time: {new Date().toISOString()}
Current time: {new Date().toISOString()}. Counter:{' '}
{c.previousState.counter}
</Heading>
</VStack>
),
Expand Down
2 changes: 2 additions & 0 deletions site/pages/concepts/image-handler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ app.frame('/', (c) => { // [!code focus]
})

app.image('/img', (c) => { // [!code focus]
/* Access frame's state via `c.previousState` */
return c.res({/* ... */})
})
```
Expand Down Expand Up @@ -51,6 +52,7 @@ app.frame('/', (c) => {
})

app.image('/img', (c) => {
/* Access frame's state via `c.previousState` */
return c.res({
headers: { // [!code focus]
'Cache-Control': 'max-age=0' // [!code focus]
Expand Down
51 changes: 51 additions & 0 deletions site/pages/reference/frog-image-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,57 @@ app.image('/', (c) => { // [!code focus]
An image handler can also be asynchronous (ie. `async (c) => { ... }{:js}`).
:::

## previousButtonValues

- **Type**: `string[]`

The data of the previous intents.

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog({
title: 'Frog Frame'
})

app.frame('/', (c) => {
const { previousButtonValues } = c // [!code focus]
return c.res({/* ... */})
})
```

## previousState

- **Type**: `State`

The state of the previous frame.

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

type State = {
values: string[]
}

export const app = new Frog<{ State: State }>({
initialState: {
values: []
},
title: 'Frog Frame',
})

app.frame('/', (c) => {
const { previousState } = c // [!code focus]
return c.res({/* ... */})
})
```

## req

- **Type**: `Request`
Expand Down
16 changes: 14 additions & 2 deletions src/frog-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { parseImage } from './utils/parseImage.js'
import { parseIntents } from './utils/parseIntents.js'
import { parsePath } from './utils/parsePath.js'
import { requestBodyToContext } from './utils/requestBodyToContext.js'
import { requestBodyToImageContext } from './utils/requestBodyToImageContext.js'
import { serializeJson } from './utils/serializeJson.js'
import { toSearchParams } from './utils/toSearchParams.js'
import { version } from './version.js'
Expand Down Expand Up @@ -711,7 +712,15 @@ export class FrogBase<
return nonMiddlewareMatchedRoutes.length !== 0
})()

if (isHandlerPresentOnImagePath) return `${baseUrl + parsePath(image)}`
if (isHandlerPresentOnImagePath)
return `${baseUrl + parsePath(image)}${
context.status !== 'initial'
? `?${toSearchParams({
previousState,
previousButtonValues: buttonValues,
}).toString()}`
: ''
}`
return `${assetsUrl + parsePath(image)}`
})()

Expand Down Expand Up @@ -928,7 +937,10 @@ export class FrogBase<
const assetsUrl = origin + parsePath(this.assetsPath)

const { context } = getImageContext<env, string>({
context: c,
context: await requestBodyToImageContext(c, {
secret: this.secret,
}),
initialState: this._initialState,
})

const response = await handler(context)
Expand Down
10 changes: 10 additions & 0 deletions src/types/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ export type ImageContext<
env extends Env = Env,
path extends string = string,
input extends Input = {},
//
_state = env['State'],
> = {
/**
* `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers.
Expand All @@ -308,6 +310,14 @@ export type ImageContext<
* @see https://hono.dev/api/context#env
*/
env: Context_hono<env, path>['env']
/**
* Button values from the previous frame.
*/
previousButtonValues?: FrameButtonValue[] | undefined
/**
* State from the previous frame.
*/
previousState: _state
/**
* Hono request object.
*
Expand Down
17 changes: 11 additions & 6 deletions src/utils/getImageContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Context as Context_hono, Input } from 'hono'
import type { ImageContext } from '../types/context.js'
import type { Input } from 'hono'
import type { Context, ImageContext } from '../types/context.js'
import type { Env } from '../types/env.js'

type GetImageContextParameters<
Expand All @@ -9,7 +9,11 @@ type GetImageContextParameters<
//
_state = env['State'],
> = {
context: Context_hono<env, path, input>
context: Omit<
Context<env, path, input, _state>,
'frameData' | 'verified' | 'status' | 'initialPath'
>
initialState?: _state
}

type GetImageContextReturnType<
Expand All @@ -19,7 +23,7 @@ type GetImageContextReturnType<
//
_state = env['State'],
> = {
context: ImageContext<env, path, input>
context: ImageContext<env, path, input, _state>
}

export function getImageContext<
Expand All @@ -31,12 +35,13 @@ export function getImageContext<
>(
parameters: GetImageContextParameters<env, path, input, _state>,
): GetImageContextReturnType<env, path, input, _state> {
const { context } = parameters
const { env, req } = context || {}
const { context, initialState } = parameters
const { env, previousState, req } = context || {}

return {
context: {
env,
previousState: previousState ?? (initialState as _state),
req,
res: (data) => ({ data, format: 'image', status: 'success' }),
var: context.var,
Expand Down
56 changes: 56 additions & 0 deletions src/utils/requestBodyToImageContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Context as Context_hono, Input } from 'hono'
import type { FrogConstructorParameters } from '../frog-base.js'
import type { Context } from '../types/context.js'
import type { Env } from '../types/env.js'
import { fromQuery } from './fromQuery.js'
import { getRequestUrl } from './getRequestUrl.js'
import * as jws from './jws.js'

type RequestBodyToImageContextOptions = {
secret?: FrogConstructorParameters['secret']
}

type RequestBodyToImageContextReturnType<
env extends Env = Env,
path extends string = string,
input extends Input = {},
//
_state = env['State'],
> = Omit<
Context<env, path, input, _state>,
'frameData' | 'verified' | 'status' | 'initialPath'
>

export async function requestBodyToImageContext<
env extends Env,
path extends string,
input extends Input,
//
_state = env['State'],
>(
c: Context_hono<env, path>,
{ secret }: RequestBodyToImageContextOptions,
): Promise<RequestBodyToImageContextReturnType<env, path, input, _state>> {
const { previousState, previousButtonValues } = await (async () => {
if (c.req.query()) {
let { previousState, previousButtonValues } = fromQuery(
c.req.query(),
) as any
if (secret && previousState)
previousState = JSON.parse(await jws.verify(previousState, secret))
return { previousState, previousButtonValues }
}
return {} as any
})()

const url = getRequestUrl(c.req)

return {
env: c.env,
previousState,
previousButtonValues,
req: c.req,
url: url.href,
var: c.var,
}
}

0 comments on commit ae57791

Please sign in to comment.