Skip to content
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
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,100 @@ const auth = getSdkAuthContext(activeUser, activeUser?.username);
const { mutateAsync } = useWalletOperation(username, asset, operation, auth);
```

***

## HTML Edge Caching

The web app emits `Cache-Control` headers from Next.js middleware, and the CDN
(Cloudflare) and reverse proxy (Nginx) respect them. Next.js is the single
source of truth for cache policy — do not override it at the infra layer.

### Key files

- `apps/web/src/features/next-middleware/cache-policy.ts` — route-pattern TTLs
- `apps/web/src/middleware.ts` — header injection
- `apps/web/src/features/next-middleware/post-age-cache.ts` — per-post-age TTL refinement
- `scripts/purge-cache.sh` — manual DMCA/moderation invalidation

### Important notes

**Logged-in users never see cached HTML.** Any request carrying the
`active_user` cookie receives `Cache-Control: private, no-store`. Nginx and
the CF worker bypass cache entirely for these requests.

**No `Vary: Cookie`.** Auth bifurcation happens at the infra layer (Nginx
cache key includes `$cookie_active_user`; CF worker bypasses on the cookie).
Emitting `Vary: Cookie` would fragment the edge cache on every unrelated
cookie (analytics, locale, experiments) and destroy hit ratio.

**Post pages use age-based TTLs.** Fresh posts (< 1 day) cache for 1 minute;
posts older than 60 days cache for 30 days. The middleware starts with a
conservative 1h tier, then refines once the post's `created` date is known
via a background-populated in-memory cache (per edge isolate).

**Observability header.** Every response carries `x-cache-tier: <tier>` (or
`logged-in`) so CF analytics, Nginx logs, and DevTools reveal which policy
was applied. Use this to verify cache behavior without inspecting
`Cache-Control` directly.

### DMCA / moderation invalidation

CF edge serves cached HTML for up to the `s-maxage` window (1h for post
pages, 24h for static pages). For takedowns:

Comment thread
coderabbitai[bot] marked this conversation as resolved.
1. Update `apps/web/public/dmca/dmca-*.json`, commit, deploy
2. Run `./scripts/purge-cache.sh <affected-urls>` to drop pre-takedown HTML
from the CF edge cache

Without step 2, CF continues serving the old content until `s-maxage`
expires.

### Verifying cache behavior

```bash
# Anonymous — should HIT after the first request
curl -sI https://ecency.com/discover | grep -iE 'cache|tier'

# Logged-in — should always BYPASS
curl -sI --cookie "active_user=alice" https://ecency.com/discover | grep -iE 'cache|tier'
```

Expected headers on an anonymous hit:

```http
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=3600
X-Cache-Tier: list
X-Cache-Status: HIT
CF-Cache-Status: HIT
```

### Infra configuration

Nginx and CF worker configs live in the infra repo, not here. The rules are
simple: **respect origin `Cache-Control`**, **bypass on `active_user`
cookie**, and **preserve `x-cache-tier`** in the response headers.


| Tier | `s-maxage` | `stale-while-revalidate` | Routes |
|---|---|---|---|
| `static` | 24h | 7d | `/faq`, `/about`, `/child-safety`, `/contributors`, `/privacy-policy`, `/terms-of-service`, `/whitepaper`, `/mobile` |
| `home` | 5m | 1h | `/` |
| `list` | 5m | 1h | `/discover`, `/communities`, `/witnesses`, `/tags` |
| `list-proposals` | 10m | 1h | `/proposals` |
| `feed` | 1m | 5m | `/hot`, `/trending`, `/payout`, `/muted`, `/promoted` + tags |
| `feed-created` | 30s | 2m | `/created`, `/tags/:tag` |
| `community` | 1m | 5m | `/:tag/hive-xxxxx` |
| `profile` | 5m | 1h | `/@author`, `/@author/posts`, `/blog`, `/comments`, `/replies`, `/communities`, `/insights` |
| `profile-feed` | 1m | 5m | `/@author/feed`, `/@author/trail` (aggregates other users' content) |
| `entry` | 1h | 1d | post pages (default — used until post age is known) |
| `entry-fresh` | 1m | 5m | posts < 1 day old |
| `entry-week` | 1h | 1d | posts 1-7 days old |
| `entry-month` | 1d | 7d | posts 7-30 days old |
| `entry-archive` | 30d | 7d | posts 30-60 days old |
| `entry-ancient` | 30d | 60d | posts > 60 days old |
| `no-cache` | 0 | 0 | `/publish`, `/chats`, `/auth/*`, `/wallet`, `/@author/settings`, etc. |


***

## Build instructions
Expand Down
2 changes: 1 addition & 1 deletion apps/self-hosted/hosting/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"db:migrate": "tsx src/db/migrate.ts"
},
"dependencies": {
"@ecency/hive-tx": "^7.1.1",
"@ecency/hive-tx": "^7.3.4",
"@hiveio/x402": "^0.1.2",
"hono": "^4.4.0",
"pg": "^8.12.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/self-hosted/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@ecency/sdk": "workspace:*",
"@ecency/ui": "workspace:*",
"@ecency/wallets": "workspace:*",
"@ecency/hive-tx": "^7.1.1",
"@ecency/hive-tx": "^7.3.4",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-router": "^1.132.27",
"@tiptap/core": "^2.11.5",
Expand Down
14 changes: 7 additions & 7 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@floating-ui/dom": "^1.6.13",
"@floating-ui/react-dom": "^2.1.2",
"@hello-pangea/dnd": "^18.0.1",
"@ecency/hive-tx": "^7.3.3",
"@ecency/hive-tx": "^7.3.4",
"@hiveio/hivescript": "^1.3.3",
"@hookform/resolvers": "^5.2.0",
"@joplin/turndown-plugin-gfm": "^1.0.62",
Expand Down Expand Up @@ -84,7 +84,7 @@
"dompurify": "^3.2.5",
"emoji-mart": "^5.5.2",
"formidable": "^3.5.4",
"framer-motion": "^11.3.24",
"framer-motion": "^11.18.2",
"heic2any": "^0.0.4",
"highcharts": "^12.1.2",
"highcharts-react-official": "^3.2.1",
Expand All @@ -106,15 +106,15 @@
"numeral": "^2.0.6",
"path-to-regexp": "6.2.1",
"qrcode": "^1.5.3",
"react": "18.3.1",
"react": "^19.1.1",
"react-countdown": "^2.3.6",
"react-day-picker": "^9.9.0",
"react-dom": "18.3.1",
"react-dom": "^19.1.1",
"react-fast-compare": "^3.2.2",
"react-google-recaptcha": "^3.1.0",
"react-grid-layout": "^1.4.4",
"react-hook-form": "^7.61.1",
"react-in-viewport": "1.0.0-beta.6",
"react-in-viewport": "1.0.0-beta.9",
"react-resizable": "^3.0.5",
"react-resize-detector": "^7.1.2",
"react-sortablejs": "^6.1.4",
Expand Down Expand Up @@ -157,8 +157,8 @@
"@types/numeral": "^2.0.5",
"@types/path-to-regexp": "^1.7.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@types/react-google-recaptcha": "^2.1.8",
"@types/react-grid-layout": "^1.3.5",
"@types/react-virtualized": "^9.21.30",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/public/sw.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function Step3ReviewKeys({ mode = "add", initialSelectedKey, onNext, onBa
const { activeUser } = useActiveAccount();
const [selectedKeys, setSelectedKeys] = useState<SelectedKeysMap>(new Map());
const getDerivation = useKeyDerivationStore((state) => state.getDerivation);
const seededKeyRef = useRef<string | undefined>();
const seededKeyRef = useRef<string | undefined>(undefined);

const username = activeUser?.username;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export function HiveEngineChart() {
const isSpkLayerToken = isSpkLayerTokenSymbol(tokenSymbol);
const { ref: chartContainerRef } = useResizeDetector();

const chartRef = useRef<IChartApi>();
const candleStickSeriesRef = useRef<ISeriesApi<"Candlestick">>();
const chartRef = useRef<IChartApi | null>(null);
const candleStickSeriesRef = useRef<ISeriesApi<"Candlestick"> | null>(null);

const { data } = useQuery({
...getHiveEngineTokensMetricsQueryOptions(tokenSymbol ?? "", "hourly"),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/_components/download-trigger/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment, useState } from "react";
import React, { Fragment, JSX, useState } from "react";
import "./_index.scss";
import { Modal, ModalBody } from "@ui/modal";
import useMount from "react-use/lib/useMount";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DeckHeader } from "../../header/deck-header";
import React, { useContext, useState } from "react";
import React, { JSX, useContext, useState } from "react";
import { DeckGridItem } from "../../types";
import "./_deck-add-column.scss";
import { DeckAddColumnTypeSettings } from "./deck-add-column-type-settings";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment, useEffect, useRef, useState } from "react";
import React, { Fragment, JSX, useEffect, useRef, useState } from "react";
import { catchPostImage, postBodySummary, proxifyImageSrc } from "@ecency/render-helper";
import { useInViewport } from "react-in-viewport";
import { commentSvg, voteSvg } from "../../icons";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useResizeDetector } from "react-resize-detector";
import React, { useEffect, useState } from "react";
import React, { JSX, useEffect, useState } from "react";
import { IdentifiableEntry } from "../deck-threads-manager";
import { DeckThreadItemBody } from "./deck-thread-item-body";
import { useInViewport } from "react-in-viewport";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const DeckThreadsColumnComponent = ({ id, settings, draggable }: Props) => {
const previousEditingEntry = usePrevious(currentEditingEntry);

const { updateColumnIntervalMs } = useContext(DeckGridContext);
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

useMount(() => {
register(id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, FunctionComponent, PropsWithChildren } from "react";
import React, { createContext, FunctionComponent, JSX, PropsWithChildren } from "react";
import { ThreadItemEntry } from "./identifiable-entry";
import { communityThreadsQuery } from "./community-api";
import { threadsQuery } from "./threads-api";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useState } from "react";
import React, { createContext, JSX, useState } from "react";
import useDebounce from "react-use/lib/useDebounce";

export * from "./identifiable-entry";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React, { JSX, useContext } from "react";
import { DeckGridContext } from "../deck-manager";
import { DeckHeader } from "../header/deck-header";
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, { JSX, useEffect, useMemo, useRef, useState } from "react";
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
import { DeckProps, GenericDeckColumn } from "./generic-deck-column";
import { noContentSvg } from "../icons";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/decks/_components/deck-manager.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { JSX, useEffect, useState } from "react";
import { DEFAULT_LAYOUT } from "./consts";
import { DeckGrid, DeckGridItem, DeckGrids } from "./types";
import * as uuid from "uuid";
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/app/decks/_components/deck-smooth-scroller.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import React, { JSX, useCallback, useContext, useEffect, useRef, useState } from "react";
import useQueue from "react-use/lib/useQueue";
import { DeckGridContext } from "./deck-manager";

Expand All @@ -13,7 +13,7 @@ export const DeckSmoothScroller = ({ children }: Props) => {
const [startTouchX, setStartTouchX] = useState<number | undefined>(undefined);

const queue = useQueue<number | undefined>();
let wheelTimeoutRef = useRef<any>();
const wheelTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const getColumnWidth = () => {
const anyDeckColumn = document.querySelector(".deck");
Expand Down Expand Up @@ -56,7 +56,9 @@ export const DeckSmoothScroller = ({ children }: Props) => {
}
}
}
clearTimeout(wheelTimeoutRef.current);
if (wheelTimeoutRef.current) {
clearTimeout(wheelTimeoutRef.current);
}
wheelTimeoutRef.current = setTimeout(() => queue.remove(), 400);
}, [queue.first, offset, queue]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useState } from "react";
import React, { createContext, JSX, useState } from "react";
import { getAccountPostsQueryOptions } from "@ecency/sdk";
import { useDataLimit } from "@/utils/data-limit";
import { useCommunityApi, useThreadsApi } from "./api";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Props {
export const DeckThreadsFormToolbarImagePicker = ({ onAddImage }: Props) => {
const { activeUser } = useActiveAccount();

const fileInputRef = useRef<any>();
const fileInputRef = useRef<any>(null);

const [imagePickInitiated, setImagePickInitiated] = useState(false);
const [galleryPickInitiated, setGalleryPickInitiated] = useState(false);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/decks/_components/header/deck-header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { JSX, useState } from "react";
import {
chevronDownSvgForSlider,
chevronUpSvgForSlider,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { JSX } from "react";
import { Button } from "@ui/button";
import { Accordion, AccordionCollapse, AccordionToggle } from "@ui/accordion";
import { chevronDownSvgForSlider, chevronUpSvgForSlider } from "@ui/svg";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { JSX, useState } from "react";
import { MarketAdvancedModeWidgetHeader } from "./market-advanced-mode-widget-header";
import { Widget } from "@/app/market/advanced/_advanced-mode/types/layout.type";
import i18next from "i18next";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const TradingViewWidget = ({ widgetTypeChanged }: Props) => {
const theme = useGlobalStore((s) => s.theme);

const { ref: chartContainerRef, width, height } = useResizeDetector();
const chartRef = useRef<IChartApi>();
const chartRef = useRef<IChartApi | null>(null);

const [bucketSeconds, setBucketSeconds] = useLocalStorage<number>(PREFIX + "_amml_tv_bs", 300);
const [candleStickSeries, setCandleStickSeries] = useState<ISeriesApi<"Candlestick">>();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/waves/_components/waves-list-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function WavesListView({ host, feedType, username }: Props) {
}, [feedType, host, tag, username]);

const { data, fetchNextPage, isError, error, hasNextPage, refetch } = useInfiniteQuery(queryOptions);
const previousErrorMessage = useRef<string>();
const previousErrorMessage = useRef<string | undefined>(undefined);

useEffect(() => {
if (!isError || !error) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/i18n/helper.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { JSX } from "react";
import xss from "xss";
import i18next from "i18next";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const HiveMarketRateListener = ({
const [buyOrderBook, setBuyOrderBook] = useState<OrdersDataItem[]>([]);
const [sellOrderBook, setSellOrderBook] = useState<OrdersDataItem[]>([]);

const updateIntervalRef = useRef<ReturnType<typeof setInterval>>();
const updateIntervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
const fetchOrderBookRef = useRef<() => Promise<void>>(async () => {});
const isFetchingRef = useRef(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { JSX, useEffect, useMemo, useState } from "react";
import numeral from "numeral";
import i18next from "i18next";

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/shared/feedback/feedback-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface Props {
}

export function FeedbackMessage({ feedback, onClose }: Props) {
const timeoutRef = useRef<any>();
const timeoutRef = useRef<any>(null);
// Use global store directly instead of useActiveAccount() to avoid QueryClient dependency
// We only need the username for the purchase link, not the full account data
const activeUser = useGlobalStore((s) => s.activeUser);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Props {

export const PurchaseQrBuilder = ({ queryType, queryProductId, username: propUsername }: Props) => {
const [username, setUsername] = useState(propUsername ?? "");
const qrImgRef = useRef<HTMLImageElement | undefined>();
const qrImgRef = useRef<HTMLImageElement | undefined>(undefined);
const [isQrShow, setIsQrShow] = useState(false);
const [type, setType] = useState(PurchaseTypes.BOOST);
const [pointsValue, setPointsValue] = useState("999points");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/shared/scroll-to-top/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import useMount from "react-use/lib/useMount";
import useUnmount from "react-use/lib/useUnmount";

export function ScrollToTop() {
const timerRef = useRef<any>();
const timerRef = useRef<any>(undefined);
const buttonRef = useRef<HTMLDivElement | null>(null);

useMount(() => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/shared/suggestion-list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { CSSProperties, useEffect, useRef, useState } from "react";
import React, { CSSProperties, JSX, useEffect, useRef, useState } from "react";
import "./_index.scss";
import { classNameObject } from "@ui/util";
import useClickAway from "react-use/lib/useClickAway";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const VideoUpload = (props: Props & React.HTMLAttributes<HTMLDivElement>)

const videoRef = useRef<HTMLVideoElement>(null);
const thumbnailInputRef = useRef<HTMLInputElement>(null);
const selectedFileUrlRef = useRef<string>();
const selectedFileUrlRef = useRef<string | undefined>(undefined);
const dialogOpenRef = useRef(false);

const [selectedFile, setSelectedFile] = useState<string | null>(null);
Expand Down
Loading