Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: image handler previous state #427

Merged
merged 4 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
}
}
Loading