Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿

Remove redis

yoginth.com ccb1248d 24d79606

verified
+18 -568
-5
apps/api/.env.example
··· 4 4 EVER_ACCESS_KEY="" 5 5 EVER_ACCESS_SECRET="" 6 6 SHARED_SECRET="" 7 - EVENTS_DISCORD_WEBHOOK_URL="" 8 - PAGEVIEWS_DISCORD_WEBHOOK_URL="" 9 - COLLECTS_DISCORD_WEBHOOK_URL="" 10 - LIKES_DISCORD_WEBHOOK_URL="" 11 - REDIS_URL=""
-3
apps/api/env.d.ts
··· 6 6 EVER_ACCESS_KEY: string; 7 7 EVER_ACCESS_SECRET: string; 8 8 SHARED_SECRET: string; 9 - EVENTS_DISCORD_WEBHOOK_URL: string; 10 - COLLECTS_DISCORD_WEBHOOK_URL: string; 11 - LIKES_DISCORD_WEBHOOK_URL: string; 12 9 } 13 10 }
-1
apps/api/package.json
··· 21 21 "dotenv": "^17.2.2", 22 22 "hono": "^4.9.9", 23 23 "hono-rate-limiter": "^0.4.2", 24 - "ioredis": "^5.8.0", 25 24 "jose": "^6.1.0", 26 25 "linkedom": "^0.18.12", 27 26 "pg-promise": "^12.1.3",
-45
apps/api/src/collects.ts
··· 1 - import { Status } from "@hey/data/enums"; 2 - import { withPrefix } from "@hey/helpers/logger"; 3 - import type { Context } from "hono"; 4 - import enqueueDiscordWebhook from "./utils/discordQueue"; 5 - 6 - const log = withPrefix("[API]"); 7 - 8 - interface CollectsBody { 9 - slug?: string; 10 - } 11 - 12 - const collects = async (ctx: Context) => { 13 - let body: CollectsBody = {}; 14 - try { 15 - body = (await ctx.req.json()) as CollectsBody; 16 - } catch { 17 - body = {}; 18 - } 19 - 20 - const host = ctx.req.header("host") ?? ""; 21 - 22 - if (host.includes("localhost")) { 23 - return ctx.json({ 24 - data: { ok: true, skipped: true }, 25 - status: Status.Success 26 - }); 27 - } 28 - 29 - try { 30 - const item = { 31 - createdAt: Date.now(), 32 - kind: "collect" as const, 33 - payload: { slug: body.slug }, 34 - retries: 0 35 - }; 36 - 37 - void enqueueDiscordWebhook(item); 38 - } catch (err) { 39 - log.error("Failed to enqueue collect webhook", err as Error); 40 - } 41 - 42 - return ctx.json({ data: { ok: true }, status: Status.Success }); 43 - }; 44 - 45 - export default collects;
-29
apps/api/src/index.ts
··· 3 3 import { Status } from "@hey/data/enums"; 4 4 import { withPrefix } from "@hey/helpers/logger"; 5 5 import { Hono } from "hono"; 6 - import collects from "./collects"; 7 6 import authContext from "./context/authContext"; 8 - import likes from "./likes"; 9 - import authMiddleware from "./middlewares/authMiddleware"; 10 7 import cors from "./middlewares/cors"; 11 - import rateLimiter from "./middlewares/rateLimiter"; 12 - import posts from "./posts"; 13 8 import cronRouter from "./routes/cron"; 14 9 import metadataRouter from "./routes/metadata"; 15 10 import oembedRouter from "./routes/oembed"; 16 11 import ogRouter from "./routes/og"; 17 12 import ping from "./routes/ping"; 18 - import { 19 - startDiscordWebhookWorkerCollects, 20 - startDiscordWebhookWorkerLikes, 21 - startDiscordWebhookWorkerPosts 22 - } from "./workers/discordWebhook"; 23 13 24 14 const log = withPrefix("[API]"); 25 15 ··· 33 23 app.route("/metadata", metadataRouter); 34 24 app.route("/oembed", oembedRouter); 35 25 app.route("/og", ogRouter); 36 - app.post("/posts", rateLimiter({ requests: 10 }), authMiddleware, posts); 37 - app.post("/likes", rateLimiter({ requests: 20 }), authMiddleware, likes); 38 - app.post("/collects", rateLimiter({ requests: 20 }), authMiddleware, collects); 39 26 40 27 app.notFound((ctx) => 41 28 ctx.json({ error: "Not Found", status: Status.Error }, 404) ··· 44 31 serve({ fetch: app.fetch, port: 4784 }, (info) => { 45 32 log.info(`Server running on port ${info.port}`); 46 33 }); 47 - 48 - if (process.env.REDIS_URL) { 49 - const hasEvents = !!process.env.EVENTS_DISCORD_WEBHOOK_URL; 50 - const hasLikes = !!process.env.LIKES_DISCORD_WEBHOOK_URL; 51 - const hasCollects = !!process.env.COLLECTS_DISCORD_WEBHOOK_URL; 52 - 53 - if (hasEvents) void startDiscordWebhookWorkerPosts(); 54 - if (hasLikes) void startDiscordWebhookWorkerLikes(); 55 - if (hasCollects) void startDiscordWebhookWorkerCollects(); 56 - 57 - if (!hasEvents && !hasLikes && !hasCollects) { 58 - log.warn("Discord workers not started: missing webhook envs"); 59 - } 60 - } else { 61 - log.warn("Discord workers not started: missing Redis env"); 62 - }
-45
apps/api/src/likes.ts
··· 1 - import { Status } from "@hey/data/enums"; 2 - import { withPrefix } from "@hey/helpers/logger"; 3 - import type { Context } from "hono"; 4 - import enqueueDiscordWebhook from "./utils/discordQueue"; 5 - 6 - const log = withPrefix("[API]"); 7 - 8 - interface LikesBody { 9 - slug?: string; 10 - } 11 - 12 - const likes = async (ctx: Context) => { 13 - let body: LikesBody = {}; 14 - try { 15 - body = (await ctx.req.json()) as LikesBody; 16 - } catch { 17 - body = {}; 18 - } 19 - 20 - const host = ctx.req.header("host") ?? ""; 21 - 22 - if (host.includes("localhost")) { 23 - return ctx.json({ 24 - data: { ok: true, skipped: true }, 25 - status: Status.Success 26 - }); 27 - } 28 - 29 - try { 30 - const item = { 31 - createdAt: Date.now(), 32 - kind: "like" as const, 33 - payload: { slug: body.slug }, 34 - retries: 0 35 - }; 36 - 37 - void enqueueDiscordWebhook(item); 38 - } catch (err) { 39 - log.error("Failed to enqueue like webhook", err as Error); 40 - } 41 - 42 - return ctx.json({ data: { ok: true }, status: Status.Success }); 43 - }; 44 - 45 - export default likes;
-46
apps/api/src/posts.ts
··· 1 - import { Status } from "@hey/data/enums"; 2 - import { withPrefix } from "@hey/helpers/logger"; 3 - import type { Context } from "hono"; 4 - import enqueueDiscordWebhook from "./utils/discordQueue"; 5 - 6 - const log = withPrefix("[API]"); 7 - 8 - interface PostsBody { 9 - slug?: string; 10 - type?: string; 11 - } 12 - 13 - const posts = async (ctx: Context) => { 14 - let body: PostsBody = {}; 15 - try { 16 - body = (await ctx.req.json()) as PostsBody; 17 - } catch { 18 - body = {}; 19 - } 20 - 21 - const host = ctx.req.header("host") ?? ""; 22 - 23 - if (host.includes("localhost")) { 24 - return ctx.json({ 25 - data: { ok: true, skipped: true }, 26 - status: Status.Success 27 - }); 28 - } 29 - 30 - try { 31 - const item = { 32 - createdAt: Date.now(), 33 - kind: "post" as const, 34 - payload: { slug: body.slug, type: body.type }, 35 - retries: 0 36 - }; 37 - 38 - void enqueueDiscordWebhook(item); 39 - } catch (err) { 40 - log.error("Failed to enqueue post webhook", err as Error); 41 - } 42 - 43 - return ctx.json({ data: { ok: true }, status: Status.Success }); 44 - }; 45 - 46 - export default posts;
-50
apps/api/src/utils/discordQueue.ts
··· 1 - import { withPrefix } from "@hey/helpers/logger"; 2 - import { 3 - DISCORD_QUEUE_COLLECTS, 4 - DISCORD_QUEUE_LIKES, 5 - DISCORD_QUEUE_POSTS, 6 - getRedis 7 - } from "./redis"; 8 - 9 - export interface DiscordQueueItemBase { 10 - createdAt: number; 11 - retries?: number; 12 - } 13 - 14 - export interface PostQueueItem extends DiscordQueueItemBase { 15 - kind: "post"; 16 - payload: { slug?: string; type?: string }; 17 - } 18 - 19 - export interface LikeQueueItem extends DiscordQueueItemBase { 20 - kind: "like"; 21 - payload: { slug?: string }; 22 - } 23 - 24 - export interface CollectQueueItem extends DiscordQueueItemBase { 25 - kind: "collect"; 26 - payload: { slug?: string }; 27 - } 28 - 29 - export type DiscordQueueItem = PostQueueItem | LikeQueueItem | CollectQueueItem; 30 - 31 - export const enqueueDiscordWebhook = async ( 32 - item: DiscordQueueItem 33 - ): Promise<void> => { 34 - const log = withPrefix("[API]"); 35 - try { 36 - const redis = getRedis(); 37 - const key = 38 - item.kind === "post" 39 - ? DISCORD_QUEUE_POSTS 40 - : item.kind === "like" 41 - ? DISCORD_QUEUE_LIKES 42 - : DISCORD_QUEUE_COLLECTS; 43 - await redis.rpush(key, JSON.stringify(item)); 44 - log.info(`Enqueued discord webhook: ${item.kind}`); 45 - } catch (err) { 46 - log.error("Failed to enqueue discord webhook", err as Error); 47 - } 48 - }; 49 - 50 - export default enqueueDiscordWebhook;
-19
apps/api/src/utils/redis.ts
··· 1 - import IORedis from "ioredis"; 2 - 3 - let redisClient: IORedis | null = null; 4 - 5 - export const DISCORD_QUEUE_POSTS = "hey:discord:webhooks:posts"; 6 - export const DISCORD_QUEUE_LIKES = "hey:discord:webhooks:likes"; 7 - export const DISCORD_QUEUE_COLLECTS = "hey:discord:webhooks:collects"; 8 - 9 - export const getRedis = (): IORedis => { 10 - if (redisClient) return redisClient; 11 - 12 - const url = process.env.REDIS_URL; 13 - if (!url) { 14 - throw new Error("Redis not configured. Set REDIS_URL"); 15 - } 16 - 17 - redisClient = new IORedis(url); 18 - return redisClient; 19 - };
-68
apps/api/src/workers/discord/rateLimit.ts
··· 1 - import type IORedis from "ioredis"; 2 - 3 - export const DELAYED_QUEUE_KEY = "hey:discord:webhooks:delayed" as const; 4 - 5 - declare global { 6 - var __heyWebhookNextAt: Map<string, number> | undefined; 7 - } 8 - 9 - const nextAtMap = (): Map<string, number> => 10 - (globalThis.__heyWebhookNextAt ??= new Map<string, number>()); 11 - 12 - export const getWaitMs = (webhookUrl: string, now = Date.now()): number => { 13 - const until = nextAtMap().get(webhookUrl) ?? 0; 14 - return until > now ? until - now : 0; 15 - }; 16 - 17 - export const setNextIn = (webhookUrl: string, delayMs: number): number => { 18 - const until = Date.now() + Math.max(0, Math.ceil(delayMs)); 19 - const map = nextAtMap(); 20 - const prev = map.get(webhookUrl) ?? 0; 21 - const value = Math.max(prev, until); 22 - map.set(webhookUrl, value); 23 - return value; 24 - }; 25 - 26 - export const updateFromHeaders = (webhookUrl: string, res: Response): void => { 27 - const remaining = Number.parseInt( 28 - res.headers.get("x-ratelimit-remaining") ?? "NaN", 29 - 10 30 - ); 31 - const resetAfterSec = Number.parseFloat( 32 - res.headers.get("x-ratelimit-reset-after") ?? "NaN" 33 - ); 34 - if ( 35 - Number.isFinite(remaining) && 36 - remaining <= 0 && 37 - Number.isFinite(resetAfterSec) 38 - ) { 39 - setNextIn(webhookUrl, resetAfterSec * 1000); 40 - } 41 - }; 42 - 43 - export const promoteDue = async ( 44 - r: IORedis, 45 - key: string, 46 - limit = 100 47 - ): Promise<number> => { 48 - const now = Date.now(); 49 - const due = await r.zrangebyscore(key, 0, now, "LIMIT", 0, limit); 50 - if (due.length === 0) return 0; 51 - const multi = r.multi(); 52 - for (const value of due) { 53 - multi.zrem(key, value); 54 - multi.rpush(key.replace(":delayed", ""), value); 55 - } 56 - await multi.exec(); 57 - return due.length; 58 - }; 59 - 60 - export const schedule = async ( 61 - r: IORedis, 62 - key: string, 63 - item: unknown, 64 - delayMs: number 65 - ): Promise<void> => { 66 - const ts = Date.now() + Math.max(0, Math.ceil(delayMs)); 67 - await r.zadd(key, String(ts), JSON.stringify(item)); 68 - };
-46
apps/api/src/workers/discord/webhook.ts
··· 1 - import type { 2 - CollectQueueItem, 3 - DiscordQueueItem, 4 - LikeQueueItem, 5 - PostQueueItem 6 - } from "../../utils/discordQueue"; 7 - 8 - export type WebhookDetails = { webhookUrl?: string; body: unknown }; 9 - 10 - const postContent = (payload: PostQueueItem["payload"]) => { 11 - const postUrl = payload.slug ? `https://hey.xyz/posts/${payload.slug}` : ""; 12 - const type = payload.type ?? "post"; 13 - return { content: `New ${type} on Hey ${postUrl}`.trim() }; 14 - }; 15 - 16 - const likeContent = (payload: LikeQueueItem["payload"]) => { 17 - const postUrl = payload.slug ? `https://hey.xyz/posts/${payload.slug}` : ""; 18 - return { content: `New like on Hey ${postUrl}`.trim() }; 19 - }; 20 - 21 - const collectContent = (payload: CollectQueueItem["payload"]) => { 22 - const postUrl = payload.slug ? `https://hey.xyz/posts/${payload.slug}` : ""; 23 - return { content: `New collect on Hey ${postUrl}`.trim() }; 24 - }; 25 - 26 - export const resolveWebhook = (item: DiscordQueueItem): WebhookDetails => { 27 - if (item.kind === "post") { 28 - return { 29 - body: postContent(item.payload), 30 - webhookUrl: process.env.EVENTS_DISCORD_WEBHOOK_URL 31 - }; 32 - } 33 - if (item.kind === "collect") { 34 - return { 35 - body: collectContent((item as CollectQueueItem).payload), 36 - webhookUrl: process.env.COLLECTS_DISCORD_WEBHOOK_URL 37 - }; 38 - } 39 - if (item.kind === "like") { 40 - return { 41 - body: likeContent((item as any).payload), 42 - webhookUrl: process.env.LIKES_DISCORD_WEBHOOK_URL 43 - }; 44 - } 45 - return { body: {}, webhookUrl: undefined }; 46 - };
-165
apps/api/src/workers/discordWebhook.ts
··· 1 - import { withPrefix } from "@hey/helpers/logger"; 2 - import type IORedis from "ioredis"; 3 - import type { DiscordQueueItem } from "../utils/discordQueue"; 4 - import { 5 - DISCORD_QUEUE_COLLECTS, 6 - DISCORD_QUEUE_LIKES, 7 - DISCORD_QUEUE_POSTS, 8 - getRedis 9 - } from "../utils/redis"; 10 - import { 11 - getWaitMs, 12 - promoteDue, 13 - schedule, 14 - setNextIn, 15 - updateFromHeaders 16 - } from "./discord/rateLimit"; 17 - import { resolveWebhook } from "./discord/webhook"; 18 - 19 - const log = withPrefix("[Worker]"); 20 - 21 - const sleep = (ms: number): Promise<void> => 22 - new Promise((res) => setTimeout(res, ms)); 23 - 24 - const parseItem = (raw: unknown): DiscordQueueItem | null => { 25 - if (!raw) return null; 26 - try { 27 - const item = 28 - typeof raw === "string" 29 - ? (JSON.parse(raw) as DiscordQueueItem) 30 - : (raw as DiscordQueueItem); 31 - return item && "kind" in item ? item : null; 32 - } catch (e) { 33 - log.error("Failed to parse queue item", e as Error); 34 - return null; 35 - } 36 - }; 37 - 38 - const dispatch = async (item: DiscordQueueItem) => { 39 - const { webhookUrl, body } = resolveWebhook(item); 40 - if (!webhookUrl) { 41 - log.warn(`Skipping ${item.kind} webhook: missing webhook URL env`); 42 - return { status: 0, webhookUrl: undefined as string | undefined }; 43 - } 44 - const res = await fetch(webhookUrl, { 45 - body: JSON.stringify(body), 46 - headers: { "content-type": "application/json" }, 47 - method: "POST" 48 - }); 49 - return { res, status: res.status, webhookUrl } as const; 50 - }; 51 - 52 - const startQueueWorker = async (queueKey: string, label: string) => { 53 - let redis: IORedis; 54 - try { 55 - redis = getRedis(); 56 - } catch { 57 - log.warn(`Discord worker (${label}) disabled: Redis not configured`); 58 - return; 59 - } 60 - 61 - log.info(`Discord worker started (${label}). Queue: ${queueKey}`); 62 - 63 - // eslint-disable-next-line no-constant-condition 64 - while (true) { 65 - try { 66 - const delayedKey = `${queueKey}:delayed`; 67 - await promoteDue(redis, delayedKey, 100); 68 - 69 - const res = (await redis.brpop(queueKey, 1)) as [string, string] | null; 70 - if (!res) continue; 71 - 72 - const [, raw] = res; 73 - const item = parseItem(raw); 74 - if (!item) continue; 75 - 76 - const { webhookUrl } = resolveWebhook(item); 77 - if (!webhookUrl) { 78 - log.warn(`Skipping ${item.kind} webhook: missing webhook URL env`); 79 - continue; 80 - } 81 - 82 - // If this webhook has a pending cooldown, re-schedule it individually 83 - const waitMs = getWaitMs(webhookUrl); 84 - if (waitMs > 0) { 85 - await schedule(redis, delayedKey, item, waitMs); 86 - log.warn( 87 - `Cooldown for ${item.kind}. Scheduled after ${Math.ceil(waitMs / 1000)}s` 88 - ); 89 - continue; 90 - } 91 - // Reserve 1 req/s slot pre-dispatch to avoid concurrent sends for same URL 92 - setNextIn(webhookUrl, 1000); 93 - 94 - try { 95 - const result = await dispatch(item); 96 - const { webhookUrl: url, status, res } = result; 97 - 98 - if (!url) continue; 99 - 100 - if (status === 429) { 101 - // Parse retry-after headers/body; fallback to 10s min 102 - const resetAfter = Number.parseFloat( 103 - res?.headers.get("x-ratelimit-reset-after") ?? "NaN" 104 - ); 105 - const retryAfterHeader = Number.parseFloat( 106 - res?.headers.get("retry-after") ?? "NaN" 107 - ); 108 - let retryAfterSec = Number.isFinite(resetAfter) 109 - ? resetAfter 110 - : Number.isFinite(retryAfterHeader) 111 - ? retryAfterHeader 112 - : Number.NaN; 113 - if (!Number.isFinite(retryAfterSec)) { 114 - const payload = (await res?.json().catch(() => null)) as { 115 - retry_after?: number | string; 116 - } | null; 117 - const bodyRetry = payload?.retry_after; 118 - retryAfterSec = 119 - typeof bodyRetry === "number" 120 - ? bodyRetry 121 - : Number.parseFloat(String(bodyRetry ?? "NaN")); 122 - } 123 - if (!Number.isFinite(retryAfterSec)) retryAfterSec = 10; 124 - retryAfterSec = Math.max(10, retryAfterSec); 125 - 126 - const until = setNextIn(url, retryAfterSec * 1000); 127 - const ms = Math.max(0, until - Date.now()); 128 - await schedule(redis, delayedKey, item, ms); 129 - log.warn( 130 - `Rate limited for ${item.kind}. Scheduled after ${Math.ceil(ms / 1000)}s` 131 - ); 132 - continue; 133 - } 134 - 135 - if (!res?.ok) { 136 - const text = await res?.text().catch(() => ""); 137 - throw new Error(`Discord webhook failed (${status}): ${text}`); 138 - } 139 - 140 - // Success: update from headers if provided 141 - updateFromHeaders(url, res); 142 - log.info(`Dispatched Discord webhook: ${item.kind}`); 143 - } catch (_err) { 144 - const retries = (item.retries ?? 0) + 1; 145 - if (retries <= 3) { 146 - item.retries = retries; 147 - await redis.rpush(queueKey, JSON.stringify(item)); 148 - log.warn(`Requeued ${item.kind} webhook (attempt ${retries})`); 149 - } else { 150 - log.error(`Dropped ${item.kind} webhook after ${retries} attempts`); 151 - } 152 - } 153 - } catch (e) { 154 - log.error("Discord worker loop error", e as Error); 155 - await sleep(1000); 156 - } 157 - } 158 - }; 159 - 160 - export const startDiscordWebhookWorkerPosts = async () => 161 - startQueueWorker(DISCORD_QUEUE_POSTS, "posts"); 162 - export const startDiscordWebhookWorkerLikes = async () => 163 - startQueueWorker(DISCORD_QUEUE_LIKES, "likes"); 164 - export const startDiscordWebhookWorkerCollects = async () => 165 - startQueueWorker(DISCORD_QUEUE_COLLECTS, "collects");
+2 -6
apps/web/src/components/Post/Actions/Like.tsx
··· 16 16 import { Tooltip } from "@/components/Shared/UI"; 17 17 import cn from "@/helpers/cn"; 18 18 import errorToast from "@/helpers/errorToast"; 19 - import { hono } from "@/helpers/fetcher"; 20 19 import { useAccountStore } from "@/store/persisted/useAccountStore"; 21 20 22 21 interface LikeProps { ··· 92 91 } 93 92 94 93 increment(); 95 - const res = await addReaction({ 94 + 95 + return await addReaction({ 96 96 variables: { 97 97 request: { post: post.id, reaction: PostReactionType.Upvote } 98 98 } 99 99 }); 100 - 101 - void hono.likes.create({ slug: post.slug }); 102 - 103 - return res; 104 100 }; 105 101 106 102 const iconClassName = showCount
-3
apps/web/src/components/Post/Actions/Share/Repost.tsx
··· 9 9 import { toast } from "sonner"; 10 10 import cn from "@/helpers/cn"; 11 11 import errorToast from "@/helpers/errorToast"; 12 - import { hono } from "@/helpers/fetcher"; 13 12 import useTransactionLifecycle from "@/hooks/useTransactionLifecycle"; 14 13 import { useAccountStore } from "@/store/persisted/useAccountStore"; 15 14 ··· 34 33 if (!post.operations) { 35 34 return; 36 35 } 37 - 38 - void hono.posts.create({ slug: post.slug, type: "Repost" }); 39 36 40 37 cache.modify({ 41 38 fields: {
-3
apps/web/src/components/Post/OpenAction/CollectAction/CollectActionButton.tsx
··· 14 14 import LoginButton from "@/components/Shared/LoginButton"; 15 15 import { Button, Spinner } from "@/components/Shared/UI"; 16 16 import errorToast from "@/helpers/errorToast"; 17 - import { hono } from "@/helpers/fetcher"; 18 17 import getCollectActionData from "@/helpers/getCollectActionData"; 19 18 import useTransactionLifecycle from "@/hooks/useTransactionLifecycle"; 20 19 import { useAccountStore } from "@/store/persisted/useAccountStore"; ··· 79 78 onCollectSuccess?.(); 80 79 updateCache(); 81 80 toast.success("Collected successfully"); 82 - 83 - void hono.collects.create({ slug: post.slug }); 84 81 }; 85 82 86 83 const onError = useCallback((error: ApolloClientError) => {
-21
apps/web/src/helpers/fetcher.ts
··· 62 62 }; 63 63 64 64 export const hono = { 65 - collects: { 66 - create: async (payload: { slug: string }) => 67 - fetchApi<{ ok: boolean; skipped?: boolean }>("/collects", { 68 - body: JSON.stringify(payload), 69 - method: "POST" 70 - }) 71 - }, 72 65 events: { 73 66 create: async (payload: { event: string }) => 74 67 fetch("https://yoginth.com/api/hey/events", { ··· 80 73 method: "POST" 81 74 }) 82 75 }, 83 - likes: { 84 - create: async (payload: { slug: string }) => 85 - fetchApi<{ ok: boolean; skipped?: boolean }>("/likes", { 86 - body: JSON.stringify(payload), 87 - method: "POST" 88 - }) 89 - }, 90 76 metadata: { 91 77 sts: (): Promise<STS> => { 92 78 return fetchApi<STS>("/metadata/sts", { method: "GET" }); ··· 96 82 get: (url: string): Promise<Oembed> => { 97 83 return fetchApi<Oembed>(`/oembed/get?url=${url}`, { method: "GET" }); 98 84 } 99 - }, 100 - posts: { 101 - create: async (payload: { slug: string; type?: string }) => 102 - fetchApi<{ ok: boolean; skipped?: boolean }>("/posts", { 103 - body: JSON.stringify(payload), 104 - method: "POST" 105 - }) 106 85 } 107 86 };
-3
apps/web/src/hooks/useCreatePost.tsx
··· 9 9 import { useCallback } from "react"; 10 10 import { useNavigate } from "react-router"; 11 11 import { toast } from "sonner"; 12 - import { hono } from "@/helpers/fetcher"; 13 12 import useTransactionLifecycle from "./useTransactionLifecycle"; 14 13 import useWaitForTransactionToComplete from "./useWaitForTransactionToComplete"; 15 14 ··· 43 42 } 44 43 45 44 const type = isComment ? "Comment" : "Post"; 46 - 47 - void hono.posts.create({ slug: data.post.slug, type }); 48 45 49 46 toast.success(`${type} created successfully!`, { 50 47 action: {
+16 -10
pnpm-lock.yaml
··· 53 53 hono-rate-limiter: 54 54 specifier: ^0.4.2 55 55 version: 0.4.2(hono@4.9.9) 56 - ioredis: 57 - specifier: ^5.8.0 58 - version: 5.8.0 59 56 jose: 60 57 specifier: ^6.1.0 61 58 version: 6.1.0 ··· 8234 8231 optionalDependencies: 8235 8232 '@types/node': 24.5.2 8236 8233 8237 - '@ioredis/commands@1.4.0': {} 8234 + '@ioredis/commands@1.4.0': 8235 + optional: true 8238 8236 8239 8237 '@isaacs/fs-minipass@4.0.1': 8240 8238 dependencies: ··· 11188 11186 11189 11187 clsx@2.1.1: {} 11190 11188 11191 - cluster-key-slot@1.1.2: {} 11189 + cluster-key-slot@1.1.2: 11190 + optional: true 11192 11191 11193 11192 color-convert@2.0.1: 11194 11193 dependencies: ··· 11321 11320 11322 11321 defu@6.1.4: {} 11323 11322 11324 - denque@2.1.0: {} 11323 + denque@2.1.0: 11324 + optional: true 11325 11325 11326 11326 dependency-graph@0.11.0: {} 11327 11327 ··· 11985 11985 standard-as-callback: 2.1.0 11986 11986 transitivePeerDependencies: 11987 11987 - supports-color 11988 + optional: true 11988 11989 11989 11990 iron-webcrypto@1.2.1: {} 11990 11991 ··· 12225 12226 dependencies: 12226 12227 p-locate: 4.1.0 12227 12228 12228 - lodash.defaults@4.2.0: {} 12229 + lodash.defaults@4.2.0: 12230 + optional: true 12229 12231 12230 - lodash.isarguments@3.1.0: {} 12232 + lodash.isarguments@3.1.0: 12233 + optional: true 12231 12234 12232 12235 lodash.sortby@4.7.0: {} 12233 12236 ··· 13254 13257 13255 13258 real-require@0.1.0: {} 13256 13259 13257 - redis-errors@1.2.0: {} 13260 + redis-errors@1.2.0: 13261 + optional: true 13258 13262 13259 13263 redis-parser@3.0.0: 13260 13264 dependencies: 13261 13265 redis-errors: 1.2.0 13266 + optional: true 13262 13267 13263 13268 regex-recursion@6.0.2: 13264 13269 dependencies: ··· 13536 13541 dependencies: 13537 13542 tslib: 2.8.1 13538 13543 13539 - standard-as-callback@2.1.0: {} 13544 + standard-as-callback@2.1.0: 13545 + optional: true 13540 13546 13541 13547 stream-browserify@3.0.0: 13542 13548 dependencies: