Skip to content

Commit

Permalink
feat: shorten urls (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom authored Feb 14, 2024
1 parent c034d3b commit d750497
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 66 deletions.
2 changes: 1 addition & 1 deletion examples/_dev/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Button, Farc, TextInput } from 'farc'
import { app as todoApp } from './todos'

const app = new Farc({
// basePath: '/api'
// basePath: '/api',
})

app.frame('/', (context) => {
Expand Down
43 changes: 20 additions & 23 deletions src/farc-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from './types.js'
import { deserializeJson } from './utils/deserializeJson.js'
import { getFrameContext } from './utils/getFrameContext.js'
import { getIntentData } from './utils/getIntentData.js'
import { parseIntents } from './utils/parseIntents.js'
import { parsePath } from './utils/parsePath.js'
import { requestToContext } from './utils/requestToContext.js'
Expand Down Expand Up @@ -44,6 +45,7 @@ export class FarcBase<
> {
#initialState: state = undefined as state

basePath: string
hono: Hono<env, schema, basePath>
fetch: Hono<env, schema, basePath>['fetch']
get: Hono<env, schema, basePath>['get']
Expand All @@ -56,6 +58,7 @@ export class FarcBase<
}: FarcConstructorParameters<state, env, basePath> = {}) {
this.hono = new Hono<env, schema, basePath>(honoOptions)
if (basePath) this.hono = this.hono.basePath(basePath)
this.basePath = basePath ?? '/'
this.fetch = this.hono.fetch.bind(this.hono)
this.get = this.hono.get.bind(this.hono)
this.post = this.hono.post.bind(this.hono)
Expand All @@ -67,20 +70,15 @@ export class FarcBase<
path: path,
handler: (
context: FrameContext<path, state>,
previousContext: PreviousFrameContext<path, state> | undefined,
) => FrameHandlerReturnType | Promise<FrameHandlerReturnType>,
) {
// Frame Route (implements GET & POST).
this.hono.use(parsePath(path), async (c) => {
const query = c.req.query()

const url = new URL(c.req.url)
const baseUrl = `${url.origin}${url.pathname}`

const previousContext = query.previousContext
? deserializeJson<PreviousFrameContext<path, state>>(
query.previousContext,
)
? deserializeJson<PreviousFrameContext<state>>(query.previousContext)
: undefined
const context = await getFrameContext({
context: await requestToContext(c.req),
Expand All @@ -94,22 +92,19 @@ export class FarcBase<
if (!location) throw new Error('location required to redirect')
return c.redirect(location, 302)
}
if (context.url !== parsePath(c.req.url)) return c.redirect(context.url)

if (context.url !== parsePath(c.req.url))
return c.redirect(
`${context.url}?previousContext=${query.previousContext}`,
)

const { action, imageAspectRatio, intents } = await handler(
context,
previousContext,
)
const { action, imageAspectRatio, intents } = await handler(context)
const parsedIntents = intents ? parseIntents(intents) : null
const intentData = getIntentData(parsedIntents)

const serializedContext = serializeJson(context)
const serializedPreviousContext = serializeJson({
const serializedContext = serializeJson({
...context,
intents: parsedIntents,
request: undefined,
})
const serializedPreviousContext = serializeJson({
initialUrl: context.initialUrl,
intentData,
previousState: context.deriveState(),
})

Expand Down Expand Up @@ -141,7 +136,11 @@ export class FarcBase<
<meta
property="fc:frame:post_url"
content={`${
action ? baseUrl + parsePath(action || '') : context.url
action
? url.origin +
parsePath(this.basePath) +
parsePath(action || '')
: context.url
}?${postSearch}`}
/>
{parsedIntents}
Expand Down Expand Up @@ -175,17 +174,15 @@ export class FarcBase<
this.hono.get(`${parsePath(path)}/image`, async (c) => {
const query = c.req.query()
const previousContext = query.previousContext
? deserializeJson<PreviousFrameContext<path, state>>(
query.previousContext,
)
? deserializeJson<PreviousFrameContext<state>>(query.previousContext)
: undefined
const context = await getFrameContext({
context: deserializeJson<FrameContext<path, state>>(query.context),
initialState: this.#initialState,
previousContext,
request: c.req,
})
const { image } = await handler(context, previousContext)
const { image } = await handler(context)
return new ImageResponse(image)
})
}
Expand Down
3 changes: 1 addition & 2 deletions src/farc.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Env, Schema } from 'hono'
import { routes as devRoutes } from './dev/routes.js'
import { FarcBase, type FrameHandlerReturnType } from './farc-base.js'
import type { FrameContext, PreviousFrameContext } from './types.js'
import type { FrameContext } from './types.js'

export class Farc<
state = undefined,
Expand All @@ -13,7 +13,6 @@ export class Farc<
path: path,
handler: (
context: FrameContext<path, state>,
previousContext: PreviousFrameContext<path, state> | undefined,
) => FrameHandlerReturnType | Promise<FrameHandlerReturnType>,
) {
super.frame(path, handler)
Expand Down
18 changes: 8 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// TODO: TSDoc

import { type Context, type Env } from 'hono'
import { type JSXNode } from 'hono/jsx'

export type FrameContext<path extends string = string, state = unknown> = {
buttonIndex?: number | undefined
buttonValue?: string | undefined
deriveState: (fn?: (state: state) => void) => state
frameData: FrameData
initialUrl: string
inputText?: string | undefined
previousState: state
Expand All @@ -18,17 +17,14 @@ export type FrameContext<path extends string = string, state = unknown> = {
* - `response` - The frame has been interacted with (user presses button).
*/
status: 'initial' | 'redirect' | 'response'
trustedData?: TrustedData | undefined
untrustedData?: UntrustedData | undefined
url: Context['req']['url']
}

export type PreviousFrameContext<
path extends string = string,
state = unknown,
> = FrameContext<path, state> & {
/** Intents from the previous frame. */
intents: readonly JSXNode[]
export type PreviousFrameContext<state = unknown> = {
initialUrl: string
/** Intent data from the previous frame. */
intentData: readonly Record<string, string>[]
previousState: state
}

export type FrameData = {
Expand All @@ -49,6 +45,8 @@ export type FrameVersion = 'vNext'
export type FrameIntent = JSX.Element | false | null | undefined
export type FrameIntents = FrameIntent | FrameIntent[]

export type FrameIntentData = Record<string, string>

export type TrustedData = {
messageBytes: string
}
Expand Down
23 changes: 8 additions & 15 deletions src/utils/getFrameContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,22 @@ import { getIntentState } from './getIntentState.js'
import { parsePath } from './parsePath.js'

type GetFrameContextParameters<state = unknown> = {
context: Pick<
FrameContext<string, state>,
'status' | 'trustedData' | 'untrustedData' | 'url'
>
context: Pick<FrameContext<string, state>, 'frameData' | 'status' | 'url'>
initialState?: state
previousContext: PreviousFrameContext<string, state> | undefined
previousContext: PreviousFrameContext<state> | undefined
request: Context['req']
}

export async function getFrameContext<state>(
options: GetFrameContextParameters<state>,
): Promise<FrameContext<string, state>> {
const { context, previousContext, request } = options
const { trustedData, untrustedData } = context || {}
const { frameData } = context || {}

const { buttonIndex, buttonValue, inputText, redirect, reset } =
getIntentState(
// TODO: derive from untrusted data.
untrustedData,
previousContext?.intents || [],
)
const { buttonValue, inputText, redirect, reset } = getIntentState(
frameData,
previousContext?.intentData || [],
)

const status = (() => {
if (redirect) return 'redirect'
Expand All @@ -50,16 +45,14 @@ export async function getFrameContext<state>(
}

return {
buttonIndex,
buttonValue,
frameData,
initialUrl,
inputText,
deriveState,
previousState: previousState as any,
request,
status,
trustedData,
untrustedData,
url,
}
}
25 changes: 25 additions & 0 deletions src/utils/getIntentData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { JSXNode } from 'hono/jsx'
import type { FrameIntentData } from '../types.js'

export function getIntentData(
intents: readonly JSXNode[] | null,
): FrameIntentData[] {
if (!intents) return []

const intentData: FrameIntentData[] = []
for (const intent of intents) {
if (!intent) continue
const { property } = intent.props
const data: FrameIntentData = {}
for (const [key, value] of Object.entries(intent.props)) {
if (!key.startsWith('data-')) continue
data[key] = value
}
if (Object.keys(data).length === 0) continue
intentData.push({
property,
...data,
})
}
return intentData
}
25 changes: 14 additions & 11 deletions src/utils/getIntentState.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import { type JSXNode } from 'hono/jsx'
import { type FrameData, type FrameIntentData } from '../types.js'

import { type FrameData } from '../types.js'
type IntentState = {
buttonValue: string | undefined
inputText: string | undefined
redirect: boolean
reset: boolean
}

export function getIntentState(
frameData: FrameData | undefined,
intents: readonly JSXNode[] | null,
intentData: readonly FrameIntentData[] | null,
) {
const { buttonIndex, inputText } = frameData || {}

const state = {
buttonIndex,
const state: IntentState = {
buttonValue: undefined,
inputText,
redirect: false,
reset: false,
}
if (!intents) return state
if (!intentData) return state

if (buttonIndex) {
const buttonIntents = intents.filter((intent) =>
intent?.props?.property?.match(/fc:frame:button:\d$/),
const buttonIntents = intentData.filter((intent) =>
intent?.property?.match(/fc:frame:button:\d$/),
)
const intent = buttonIntents[buttonIndex - 1]

const type = intent.props['data-type']
const type = intent['data-type']
if (type === 'redirect') state.redirect = true
else if (type === 'reset') state.reset = true

const value = intent.props['data-value']
const value = intent['data-value']
state.buttonValue = value
}

Expand Down
7 changes: 3 additions & 4 deletions src/utils/requestToContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import type { FrameContext } from '../types.js'

type RequestToContextReturnType = Pick<
FrameContext,
'status' | 'trustedData' | 'untrustedData' | 'url'
'frameData' | 'status' | 'url'
>

export async function requestToContext(
request: Context['req'],
): Promise<RequestToContextReturnType> {
const { trustedData, untrustedData } =
const { trustedData: _trustedData, untrustedData } =
(await request.json().catch(() => {})) || {}

return {
frameData: untrustedData,
status: request.method === 'POST' ? 'response' : 'initial',
trustedData,
untrustedData,
url: request.url,
}
}

0 comments on commit d750497

Please sign in to comment.