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
31 changes: 31 additions & 0 deletions apps/web/src/core/sdk-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const isMainProductionClient =
(window.location.hostname === "ecency.com" || window.location.hostname.endsWith(".ecency.com"));

const isServer = typeof window === "undefined";
// Declared ahead of the server block below, which calls the reporter at import time.
const RPC_PROXY_REPORT_MS = 5 * 60 * 1000;
const privateApiHost = isMainProductionClient
? ""
: isServer
Expand Down Expand Up @@ -72,9 +74,38 @@ if (isServer) {
headers: { "X-Ecency-Internal": proxySecret },
timeoutMs: 1600
});
startRpcProxyReport();
}
}

/**
* One `[rpc-proxy]` summary line per interval, and only when the counters
* moved since the last line, so a quiet process writes nothing after its
* first report. The SDK's fallbacks are otherwise invisible: a read that went
* to the node pool because the proxy was down, timed out or answered an
* unusable body renders the page all the same, and the cache's own counters
* cannot see a request that never reached it. The first line (five minutes
* after boot, even at zero) shows the reporter is alive. Cumulative, never
* reset; per process.
*/
function startRpcProxyReport(): void {
let last = "";
const tick = () => {
const s = ConfigManager.getServerRpcProxyStats();
const r = s.fallbackByReason;
const line =
`served=${s.served} fallback=${s.fallback} ` +
`(status=${r.status} timeout=${r.timeout} transport=${r.transport} ` +
`validate=${r.validate} parse=${r.parse}) skipped=${s.skipped}`;
if (line === last) return;
last = line;
console.log(`[rpc-proxy] ${line}`);
};
// Node's timer has unref(); a numeric handle elsewhere simply has none.
const handle = setInterval(tick, RPC_PROXY_REPORT_MS) as { unref?: () => void };
handle.unref?.();
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

// Initialize DMCA filtering immediately at module load time
// This ensures the lists are available before any React Query fetches execute
// Files are in public/dmca/ for both bundling and mobile app access
Expand Down
57 changes: 57 additions & 0 deletions apps/web/src/specs/core/sdk-init-proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,27 @@ import { afterEach, describe, expect, it, vi } from "vitest";
* present. A module with import-time side effects, so each case gets a fresh
* module registry and its own environment.
*/
const stats = {
served: 0,
fallback: 0,
skipped: 0,
fallbackByReason: { status: 0, timeout: 0, transport: 0, validate: 0, parse: 0 }
};
const manager = {
setPrivateApiHost: vi.fn(),
setImageHost: vi.fn(),
setHiveNodes: vi.fn(),
setUserAgent: vi.fn(),
setResilience: vi.fn(),
setServerRpcProxy: vi.fn(),
getServerRpcProxyStats: vi.fn(() => stats),
setDmcaLists: vi.fn()
};
vi.mock("@ecency/sdk", () => ({ ConfigManager: manager }));

const REPORT_MS = 5 * 60 * 1000;
const ON = { SSR_RPC_PROXY: "1", SSR_INTERNAL_SECRET: "s3cret", INTERNAL_API_HOST: "http://vapi:4000" };

async function load(env: Record<string, string | undefined>): Promise<void> {
vi.resetModules();
for (const [k, v] of Object.entries(env)) {
Expand All @@ -28,7 +38,16 @@ async function load(env: Record<string, string | undefined>): Promise<void> {

afterEach(() => {
vi.unstubAllEnvs();
vi.useRealTimers();
vi.restoreAllMocks();
manager.setServerRpcProxy.mockClear();
manager.getServerRpcProxyStats.mockClear();
stats.served = 0;
stats.fallback = 0;
stats.skipped = 0;
for (const reason of Object.keys(stats.fallbackByReason) as (keyof typeof stats.fallbackByReason)[]) {
stats.fallbackByReason[reason] = 0;
}
});

describe("sdk-init server rpc proxy", () => {
Expand All @@ -49,4 +68,42 @@ describe("sdk-init server rpc proxy", () => {
await load(env);
expect(manager.setServerRpcProxy).not.toHaveBeenCalled();
});

describe("periodic [rpc-proxy] report", () => {
it("prints one summary line per interval, only when the counters moved", async () => {
vi.useFakeTimers();
const log = vi.spyOn(console, "log").mockImplementation(() => {});
await load(ON);
expect(log).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(REPORT_MS);
expect(log).toHaveBeenCalledTimes(1);
expect(log).toHaveBeenLastCalledWith(
"[rpc-proxy] served=0 fallback=0 (status=0 timeout=0 transport=0 validate=0 parse=0) skipped=0"
);

// Nothing moved: silence, not a repeat.
await vi.advanceTimersByTimeAsync(REPORT_MS * 3);
expect(log).toHaveBeenCalledTimes(1);

stats.served = 41;
stats.fallback = 2;
stats.fallbackByReason.transport = 2;
stats.skipped = 7;
await vi.advanceTimersByTimeAsync(REPORT_MS);
expect(log).toHaveBeenCalledTimes(2);
expect(log).toHaveBeenLastCalledWith(
"[rpc-proxy] served=41 fallback=2 (status=0 timeout=0 transport=2 validate=0 parse=0) skipped=7"
);
});

it("does not start when the proxy is off", async () => {
vi.useFakeTimers();
const log = vi.spyOn(console, "log").mockImplementation(() => {});
await load({ ...ON, SSR_RPC_PROXY: undefined });
await vi.advanceTimersByTimeAsync(REPORT_MS * 2);
expect(manager.getServerRpcProxyStats).not.toHaveBeenCalled();
expect(log).not.toHaveBeenCalled();
});
});
});
6 changes: 6 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2.3.90

### Patch Changes

- SSR RPC proxy: report rpcProxyStats from the web tier (#1623)

## 2.3.89

### Patch Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,4 +1153,4 @@ declare namespace utils {
export { type utils_WitnessProps as WitnessProps, utils_buildWitnessSetProperties as buildWitnessSetProperties, utils_makeBitMaskFilter as makeBitMaskFilter, utils_operations as operations, utils_validateUsername as validateUsername };
}

export { type FeedPublishOperation as $, type APIMethods as A, type BroadcastResult as B, type CustomJsonOperation as C, type ClaimRewardBalanceOperation as D, type CollateralizedConvertOperation as E, type CommentOperation as F, type CommentOptionsOperation as G, type ConvertOperation as H, type CreateClaimedAccountOperation as I, type CreateProposalOperation as J, type CustomOperation as K, DEFAULT_SERVER_RPC_PROXY_METHODS as L, Memo as M, type DeclineVotingRightsOperation as N, type Operation as O, PrivateKey as P, type DelegateVestingSharesOperation as Q, type ResilienceOptions as R, type ServerRpcProxyOptions as S, Transaction as T, type DeleteCommentOperation as U, type DigestData as V, type EscrowApproveOperation as W, type EscrowDisputeOperation as X, type EscrowReleaseOperation as Y, type EscrowTransferOperation as Z, type Extension as _, type Authority as a, type LimitOrderCancelOperation as a0, type LimitOrderCreate2Operation as a1, type LimitOrderCreateOperation as a2, type Price as a3, RPCError as a4, type RecoverAccountOperation as a5, type RecurrentTransferOperation as a6, type RemoveProposalOperation as a7, type RequestAccountRecoveryOperation as a8, type ResetAccountOperation as a9, type SetResetAccountOperation as aa, type SetWithdrawVestingRouteOperation as ab, type TransactionStatus as ac, type TransactionType as ad, type TransferFromSavingsOperation as ae, type TransferOperation as af, type TransferToSavingsOperation as ag, type TransferToVestingOperation as ah, type UpdateProposalOperation as ai, type UpdateProposalVotesOperation as aj, type VoteOperation as ak, type WithdrawVestingOperation as al, type WitnessProps$1 as am, type WitnessSetPropertiesOperation as an, type WitnessSetPropertiesParams as ao, type WitnessUpdateOperation as ap, resetRpcProxyBreaker as aq, rpcProxyStats as ar, setNodes as as, setResilience as at, setRestNodes as au, setRestNodesByApi as av, setServerRpcProxy as aw, setUserAgent as ax, PublicKey as b, type OperationName as c, type AccountCreateOperation as d, type AssetSymbol as e, type OperationBody as f, Signature as g, callREST as h, callRPC as i, callRPCBroadcast as j, callWithQuorum as k, config as l, type AccountCreateWithDelegationOperation as m, type AccountUpdate2Operation as n, operations as o, type AccountUpdateOperation as p, type AccountWitnessProxyOperation as q, type AccountWitnessVoteOperation as r, type Beneficiary as s, type BroadcastError as t, utils as u, type CallResponse as v, type CancelTransferFromSavingsOperation as w, type ChainProperties as x, type ChangeRecoveryAccountOperation as y, type ClaimAccountOperation as z };
export { type Extension as $, type APIMethods as A, type BroadcastResult as B, type CustomJsonOperation as C, type ClaimAccountOperation as D, type ClaimRewardBalanceOperation as E, type CollateralizedConvertOperation as F, type CommentOperation as G, type CommentOptionsOperation as H, type ConvertOperation as I, type CreateClaimedAccountOperation as J, type CreateProposalOperation as K, type CustomOperation as L, Memo as M, DEFAULT_SERVER_RPC_PROXY_METHODS as N, type Operation as O, PrivateKey as P, type DeclineVotingRightsOperation as Q, type ResilienceOptions as R, type ServerRpcProxyOptions as S, Transaction as T, type DelegateVestingSharesOperation as U, type DeleteCommentOperation as V, type DigestData as W, type EscrowApproveOperation as X, type EscrowDisputeOperation as Y, type EscrowReleaseOperation as Z, type EscrowTransferOperation as _, type Authority as a, type FeedPublishOperation as a0, type LimitOrderCancelOperation as a1, type LimitOrderCreate2Operation as a2, type LimitOrderCreateOperation as a3, type Price as a4, RPCError as a5, type RecoverAccountOperation as a6, type RecurrentTransferOperation as a7, type RemoveProposalOperation as a8, type RequestAccountRecoveryOperation as a9, type ResetAccountOperation as aa, type SetResetAccountOperation as ab, type SetWithdrawVestingRouteOperation as ac, type TransactionStatus as ad, type TransactionType as ae, type TransferFromSavingsOperation as af, type TransferOperation as ag, type TransferToSavingsOperation as ah, type TransferToVestingOperation as ai, type UpdateProposalOperation as aj, type UpdateProposalVotesOperation as ak, type VoteOperation as al, type WithdrawVestingOperation as am, type WitnessProps$1 as an, type WitnessSetPropertiesOperation as ao, type WitnessSetPropertiesParams as ap, type WitnessUpdateOperation as aq, resetRpcProxyBreaker as ar, setNodes as as, setResilience as at, setRestNodes as au, setRestNodesByApi as av, setServerRpcProxy as aw, setUserAgent as ax, PublicKey as b, type OperationName as c, type AccountCreateOperation as d, type AssetSymbol as e, type OperationBody as f, Signature as g, callREST as h, callRPC as i, callRPCBroadcast as j, callWithQuorum as k, config as l, type AccountCreateWithDelegationOperation as m, type AccountUpdate2Operation as n, operations as o, type AccountUpdateOperation as p, type AccountWitnessProxyOperation as q, rpcProxyStats as r, type AccountWitnessVoteOperation as s, type Beneficiary as t, utils as u, type BroadcastError as v, type CallResponse as w, type CancelTransferFromSavingsOperation as x, type ChainProperties as y, type ChangeRecoveryAccountOperation as z };
2 changes: 1 addition & 1 deletion packages/sdk/dist/browser/hive.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { d as AccountCreateOperation, m as AccountCreateWithDelegationOperation, n as AccountUpdate2Operation, p as AccountUpdateOperation, q as AccountWitnessProxyOperation, r as AccountWitnessVoteOperation, e as AssetSymbol, a as Authority, s as Beneficiary, t as BroadcastError, B as BroadcastResult, v as CallResponse, w as CancelTransferFromSavingsOperation, x as ChainProperties, y as ChangeRecoveryAccountOperation, z as ClaimAccountOperation, D as ClaimRewardBalanceOperation, E as CollateralizedConvertOperation, F as CommentOperation, G as CommentOptionsOperation, H as ConvertOperation, I as CreateClaimedAccountOperation, J as CreateProposalOperation, C as CustomJsonOperation, K as CustomOperation, L as DEFAULT_SERVER_RPC_PROXY_METHODS, N as DeclineVotingRightsOperation, Q as DelegateVestingSharesOperation, U as DeleteCommentOperation, V as DigestData, W as EscrowApproveOperation, X as EscrowDisputeOperation, Y as EscrowReleaseOperation, Z as EscrowTransferOperation, _ as Extension, $ as FeedPublishOperation, a0 as LimitOrderCancelOperation, a1 as LimitOrderCreate2Operation, a2 as LimitOrderCreateOperation, M as Memo, O as Operation, f as OperationBody, c as OperationName, a3 as Price, P as PrivateKey, b as PublicKey, a4 as RPCError, a5 as RecoverAccountOperation, a6 as RecurrentTransferOperation, a7 as RemoveProposalOperation, a8 as RequestAccountRecoveryOperation, a9 as ResetAccountOperation, R as ResilienceOptions, S as ServerRpcProxyOptions, aa as SetResetAccountOperation, ab as SetWithdrawVestingRouteOperation, g as Signature, T as Transaction, ac as TransactionStatus, ad as TransactionType, ae as TransferFromSavingsOperation, af as TransferOperation, ag as TransferToSavingsOperation, ah as TransferToVestingOperation, ai as UpdateProposalOperation, aj as UpdateProposalVotesOperation, ak as VoteOperation, al as WithdrawVestingOperation, am as WitnessProps, an as WitnessSetPropertiesOperation, ao as WitnessSetPropertiesParams, ap as WitnessUpdateOperation, h as callREST, i as callRPC, j as callRPCBroadcast, k as callWithQuorum, l as config, aq as resetRpcProxyBreaker, ar as rpcProxyStats, as as setNodes, at as setResilience, au as setRestNodes, av as setRestNodesByApi, aw as setServerRpcProxy, ax as setUserAgent, u as utils } from './hive-B_MUq8sk.js';
export { d as AccountCreateOperation, m as AccountCreateWithDelegationOperation, n as AccountUpdate2Operation, p as AccountUpdateOperation, q as AccountWitnessProxyOperation, s as AccountWitnessVoteOperation, e as AssetSymbol, a as Authority, t as Beneficiary, v as BroadcastError, B as BroadcastResult, w as CallResponse, x as CancelTransferFromSavingsOperation, y as ChainProperties, z as ChangeRecoveryAccountOperation, D as ClaimAccountOperation, E as ClaimRewardBalanceOperation, F as CollateralizedConvertOperation, G as CommentOperation, H as CommentOptionsOperation, I as ConvertOperation, J as CreateClaimedAccountOperation, K as CreateProposalOperation, C as CustomJsonOperation, L as CustomOperation, N as DEFAULT_SERVER_RPC_PROXY_METHODS, Q as DeclineVotingRightsOperation, U as DelegateVestingSharesOperation, V as DeleteCommentOperation, W as DigestData, X as EscrowApproveOperation, Y as EscrowDisputeOperation, Z as EscrowReleaseOperation, _ as EscrowTransferOperation, $ as Extension, a0 as FeedPublishOperation, a1 as LimitOrderCancelOperation, a2 as LimitOrderCreate2Operation, a3 as LimitOrderCreateOperation, M as Memo, O as Operation, f as OperationBody, c as OperationName, a4 as Price, P as PrivateKey, b as PublicKey, a5 as RPCError, a6 as RecoverAccountOperation, a7 as RecurrentTransferOperation, a8 as RemoveProposalOperation, a9 as RequestAccountRecoveryOperation, aa as ResetAccountOperation, R as ResilienceOptions, S as ServerRpcProxyOptions, ab as SetResetAccountOperation, ac as SetWithdrawVestingRouteOperation, g as Signature, T as Transaction, ad as TransactionStatus, ae as TransactionType, af as TransferFromSavingsOperation, ag as TransferOperation, ah as TransferToSavingsOperation, ai as TransferToVestingOperation, aj as UpdateProposalOperation, ak as UpdateProposalVotesOperation, al as VoteOperation, am as WithdrawVestingOperation, an as WitnessProps, ao as WitnessSetPropertiesOperation, ap as WitnessSetPropertiesParams, aq as WitnessUpdateOperation, h as callREST, i as callRPC, j as callRPCBroadcast, k as callWithQuorum, l as config, ar as resetRpcProxyBreaker, r as rpcProxyStats, as as setNodes, at as setResilience, au as setRestNodes, av as setRestNodesByApi, aw as setServerRpcProxy, ax as setUserAgent, u as utils } from './hive-BCPImNej.js';
14 changes: 12 additions & 2 deletions packages/sdk/dist/browser/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as _tanstack_react_query from '@tanstack/react-query';
import { MutationKey, UseMutationOptions, QueryClient, QueryKey, InfiniteData, UseQueryOptions, UseInfiniteQueryOptions, useMutation } from '@tanstack/react-query';
import { O as Operation, P as PrivateKey, B as BroadcastResult, A as APIMethods, R as ResilienceOptions, S as ServerRpcProxyOptions, a as Authority, b as PublicKey, c as OperationName, o as operations } from './hive-B_MUq8sk.js';
export { d as AccountCreateOperation, e as AssetSymbol, C as CustomJsonOperation, T as HiveTxTransaction, M as Memo, f as OperationBody, g as Signature, h as callREST, i as callRPC, j as callRPCBroadcast, k as callWithQuorum, l as hiveTxConfig, u as hiveTxUtils } from './hive-B_MUq8sk.js';
import { O as Operation, P as PrivateKey, B as BroadcastResult, A as APIMethods, R as ResilienceOptions, S as ServerRpcProxyOptions, r as rpcProxyStats, a as Authority, b as PublicKey, c as OperationName, o as operations } from './hive-BCPImNej.js';
export { d as AccountCreateOperation, e as AssetSymbol, C as CustomJsonOperation, T as HiveTxTransaction, M as Memo, f as OperationBody, g as Signature, h as callREST, i as callRPC, j as callRPCBroadcast, k as callWithQuorum, l as hiveTxConfig, u as hiveTxUtils } from './hive-BCPImNej.js';

interface AiGenerationPrice {
aspect_ratio: string;
Expand Down Expand Up @@ -1228,6 +1228,16 @@ declare namespace ConfigManager {
* @param opts - `{ url, headers, timeoutMs, methods }` or null
*/
function setServerRpcProxy(opts: ServerRpcProxyOptions | null): void;
/**
* The live counters of that proxy path: `served` (answered by the proxy),
* `fallback` with a per-reason breakdown (the read went to the node pool
* after a proxy failure) and `skipped` (breaker open). The same object the
* call path increments, exposed here because the root build carries its own
* copy of the hive-tx internals; a consumer importing `rpcProxyStats` from
* the `/hive` entry would read a different, never-incremented instance.
* Read-only by contract: the web tier prints it, nothing resets it.
*/
function getServerRpcProxyStats(): Readonly<typeof rpcProxyStats>;
/**
* Set DMCA filtering lists
* @param lists - DMCA lists object containing accounts/tags/posts arrays
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/dist/browser/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/browser/index.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/sdk/dist/node/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/node/index.cjs.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/sdk/dist/node/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/node/index.mjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ecency/sdk",
"private": false,
"version": "2.3.89",
"version": "2.3.90",
"description": "Ecency SDK",
"repository": {
"type": "git",
Expand Down
12 changes: 12 additions & 0 deletions packages/sdk/src/hive-tx/helpers/server-rpc-proxy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { callRPC, resetRpcProxyBreaker, rpcProxyStats } from "./call";
import { config, serverRpcProxy, setServerRpcProxy, DEFAULT_SERVER_RPC_PROXY_METHODS } from "../config";
import { ConfigManager } from "../../modules/core/config";

const ORIGINAL_NODES = [...config.nodes];
const PROXY = "http://proxy.internal:4000/private-api/ssr/rpc";
Expand Down Expand Up @@ -276,3 +277,14 @@ describe("server-side RPC proxy", () => {
expect(serverRpcProxy?.methods).toEqual(DEFAULT_SERVER_RPC_PROXY_METHODS);
});
});

describe("ConfigManager.getServerRpcProxyStats", () => {
it("returns the live counters object the call path increments, not a copy", () => {
const stats = ConfigManager.getServerRpcProxyStats();
expect(stats).toBe(rpcProxyStats);
const before = stats.skipped;
rpcProxyStats.skipped += 1;
expect(stats.skipped).toBe(before + 1);
rpcProxyStats.skipped = before;
});
});
14 changes: 14 additions & 0 deletions packages/sdk/src/modules/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
setUserAgent as setHiveTxUserAgent,
setResilience as setHiveTxResilience,
setServerRpcProxy as setHiveTxServerRpcProxy,
rpcProxyStats,
type ResilienceOptions,
type ServerRpcProxyOptions,
} from "../../hive-tx";
Expand Down Expand Up @@ -329,6 +330,19 @@ export namespace ConfigManager {
setHiveTxServerRpcProxy(opts);
}

/**
* The live counters of that proxy path: `served` (answered by the proxy),
* `fallback` with a per-reason breakdown (the read went to the node pool
* after a proxy failure) and `skipped` (breaker open). The same object the
* call path increments, exposed here because the root build carries its own
* copy of the hive-tx internals; a consumer importing `rpcProxyStats` from
* the `/hive` entry would read a different, never-incremented instance.
* Read-only by contract: the web tier prints it, nothing resets it.
*/
export function getServerRpcProxyStats(): Readonly<typeof rpcProxyStats> {
return rpcProxyStats;
}

/**
* Static analysis: Check for known ReDoS-vulnerable patterns
* @param pattern - Raw regex pattern string
Expand Down
7 changes: 7 additions & 0 deletions packages/wallets/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @ecency/wallets

## 5.0.90

### Patch Changes

- Updated dependencies []:
- @ecency/sdk@2.3.90

## 5.0.89

### Patch Changes
Expand Down
Loading
Loading