Skip to content
Open
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
48 changes: 48 additions & 0 deletions .github/workflows/refresh-bundled-metadata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Refresh bundled metadata

on:
schedule:
# Mon 06:00 UTC
- cron: '0 6 * * 1'
workflow_dispatch:

jobs:
refresh:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn

- name: Install dependencies
run: yarn install --immutable

- name: Fetch metadata from configured RPCs
run: node scripts/fetch-bundled-metadata.mjs

- name: Run smoke tests
run: node --test scripts/test-bundled-metadata.mjs

- name: Open PR if metadata changed
uses: peter-evans/create-pull-request@v6
with:
branch: chore/refresh-bundled-metadata
title: 'chore: refresh bundled chain metadata'
commit-message: 'chore: refresh bundled chain metadata'
body: |
Automated weekly refresh of bundled Vara metadata.

When `(genesisHash, specVersion)` matches a key in this bundle,
`@polkadot/api` skips `state_getMetadata` on cold start —
saves ~500 ms – 1.5 s on a public RPC.
labels: chore, automated
add-paths: utils/bundled-metadata/src/data.ts
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ idea/vara-eth/indexer-db/lib/
apis/gear/lib/
apis/vara-eth/lib/
utils/util/lib/
utils/bundled-metadata/lib/

# cargo
.binpath
Expand Down
9 changes: 8 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!**/dist", "!**/dist-temp", "!**/.yarn", "!**/node_modules"]
"includes": [
"**",
"!**/dist",
"!**/dist-temp",
"!**/.yarn",
"!**/node_modules",
"!utils/bundled-metadata/src/data.ts"
]
},
"formatter": {
"enabled": true,
Expand Down
1 change: 1 addition & 0 deletions idea/gear/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@base-ui/react": "1.4.1",
"@gear-js/api": "0.45.0",
"@gear-js/bundled-metadata": "*",
"@gear-js/react-hooks": "*",
"@gear-js/sails-payload-form": "*",
"@gear-js/ui": "*",
Expand Down
61 changes: 58 additions & 3 deletions idea/gear/frontend/src/app/providers/api/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,64 @@
import type { BundledMetadata } from '@gear-js/bundled-metadata';
import { ApiProvider as GearApiProvider, type ProviderProps } from '@gear-js/react-hooks';
import { useEffect, useState } from 'react';

import { INITIAL_ENDPOINT } from '@/features/api';

const ApiProvider = ({ children }: ProviderProps) => (
<GearApiProvider initialArgs={{ endpoint: INITIAL_ENDPOINT }}>{children}</GearApiProvider>
);
type LoadState = { loaded: boolean; metadata?: BundledMetadata };

// Only preload bundled metadata for endpoints we ship a bundle for. Custom/dev RPCs
// (URL param, localhost) skip the chunk download since the key won't match anyway.
// Accepts null because localStorage[...] can return null and INITIAL_ENDPOINT inherits that.
const isBundledEndpoint = (endpoint: string | null | undefined) => endpoint == null || /vara\.network/.test(endpoint);

const USE_BUNDLED = import.meta.env.VITE_BUNDLED_METADATA !== 'false' && isBundledEndpoint(INITIAL_ENDPOINT);

// Cap the chunk-fetch wait so a stalled CDN edge / proxy / flaky mobile network
// can't strand the app on a blank render. A healthy connection delivers the
// ~1.2 MB chunk in well under 1 s; 3 s is generous before we fall through to
// the RPC-fetch path.
const CHUNK_TIMEOUT_MS = 3000;

const withTimeout = <T,>(p: Promise<T>, ms: number) => {
let timer: ReturnType<typeof setTimeout>;
const timeout = new Promise<T>((_, rej) => {
timer = setTimeout(() => rej(new Error('bundled-metadata chunk timed out')), ms);
});
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
};

// Eager-preload at module top so Vite emits a separate chunk and the network fetch
// starts at JS parse time — racing the WS handshake instead of blocking after it.
const metadataPromise: Promise<BundledMetadata | undefined> = USE_BUNDLED
? withTimeout(
import('@gear-js/bundled-metadata').then((m) => m.BUNDLED_METADATA),
CHUNK_TIMEOUT_MS,
)
: Promise.resolve(undefined);

const ApiProvider = ({ children }: ProviderProps) => {
const [state, setState] = useState<LoadState>({ loaded: !USE_BUNDLED });

useEffect(() => {
if (!USE_BUNDLED) return;
metadataPromise
.then((metadata) => setState({ loaded: true, metadata }))
.catch((error) => {
// Chunk load failed (network, parse error, deploy mismatch). Fall back
// to the original RPC-fetch path so the app never gets stuck rendering null.
console.error('bundled metadata chunk failed to load; falling back to RPC fetch', error);
setState({ loaded: true });
});
}, []);

// Bounded by the lazy-chunk fetch (~180 KB). On a healthy connection this resolves
// before the WS handshake completes; if it ever measures slower in production,
// swap to a Suspense fallback rendering the existing app shell.
if (!state.loaded) return null;

return (
<GearApiProvider initialArgs={{ endpoint: INITIAL_ENDPOINT, metadata: state.metadata }}>{children}</GearApiProvider>
);
};

export { ApiProvider };
1 change: 1 addition & 0 deletions idea/gear/squid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@gear-js/api": "0.45.0",
"@gear-js/bundled-metadata": "*",
"@polkadot/api": "16.5.6",
"@subsquid/substrate-processor": "8.8.1",
"@subsquid/substrate-runtime": "2.0.1",
Expand Down
5 changes: 3 additions & 2 deletions idea/gear/squid/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GearApi } from '@gear-js/api';
import { BUNDLED_METADATA } from '@gear-js/bundled-metadata';
import { type Store, TypeormDatabase } from '@subsquid/typeorm-store';
import { createClient, type RedisClientType } from 'redis';

Expand Down Expand Up @@ -87,11 +88,11 @@ const main = async (api: GearApi) => {
await redisClient.connect();

tempState = new TempState(redisClient, api.genesisHash.toHex());
api.disconnect();
await api.disconnect();
processor.run(new TypeormDatabase({ supportHotBlocks: true }), handler);
};

GearApi.create({ providerAddress: config.squid.rpc })
GearApi.create({ providerAddress: config.squid.rpc, metadata: BUNDLED_METADATA })
.then(main)
.catch((e) => {
console.error(e);
Expand Down
1 change: 1 addition & 0 deletions lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"idea/gear/squid",
"tools/cli",
"tools/txwrapper",
"utils/bundled-metadata",
"utils/frontend-configs",
"utils/gear-hooks",
"utils/gear-ui",
Expand Down
13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"idea/vara-eth/indexer-db",
"tools/cli",
"tools/txwrapper",
"utils/bundled-metadata",
"utils/frontend-configs",
"utils/gear-hooks",
"utils/gear-ui",
Expand All @@ -53,9 +54,9 @@
"build:gear-idea-explorer": "lerna run build --scope @gear-js/api --scope gear-idea-common --scope gear-idea-explorer --scope gear-idea-indexer-db",
"build:gear-idea-faucet": "lerna run build --scope @gear-js/api --scope gear-idea-faucet --scope gear-idea-common",
"build:gear-idea-meta-storage": "lerna run build --scope @gear-js/api --scope gear-idea-meta-storage --scope gear-idea-common",
"build:gear-idea-squid": "lerna run build --scope @gear-js/api --scope gear-idea-indexer-db --scope gear-idea-squid",
"build:gear-idea-backend": "lerna run build --scope @gear-js/api --scope gear-idea-common --scope gear-idea-explorer --scope gear-idea-faucet --scope gear-idea-indexer-db --scope gear-idea-meta-storage --scope gear-idea-squid",
"build:gear-idea-frontend": "lerna run build --scope gear-idea-frontend --scope @gear-js/frontend-configs --scope @gear-js/ui --scope @gear-js/vara-ui --scope @gear-js/react-hooks --scope @gear-js/wallet-connect --scope @gear-js/api",
"build:gear-idea-squid": "lerna run build --scope @gear-js/api --scope @gear-js/bundled-metadata --scope gear-idea-indexer-db --scope gear-idea-squid",
"build:gear-idea-backend": "lerna run build --scope @gear-js/api --scope @gear-js/bundled-metadata --scope gear-idea-common --scope gear-idea-explorer --scope gear-idea-faucet --scope gear-idea-indexer-db --scope gear-idea-meta-storage --scope gear-idea-squid",
"build:gear-idea-frontend": "lerna run build --scope gear-idea-frontend --scope @gear-js/frontend-configs --scope @gear-js/ui --scope @gear-js/vara-ui --scope @gear-js/react-hooks --scope @gear-js/wallet-connect --scope @gear-js/api --scope @gear-js/bundled-metadata",
"build:varaeth-idea-frontend": "lerna run build --scope varaeth-idea-frontend --scope @gear-js/frontend-configs --scope @gear-js/api --scope @vara-eth/api --scope @gear-js/sails-payload-form",
"build:varaeth-idea-indexer-db": "lerna run build --scope @vara-eth/idea-indexer-db",
"build:varaeth-idea-indexer": "lerna run build --scope @vara-eth/idea-indexer-db --scope @vara-eth/idea-indexer",
Expand Down Expand Up @@ -84,7 +85,10 @@
"bump:polkadot": "node scripts/update-deps.mjs polkadot && yarn install",
"bump:gear-api": "node scripts/update-deps.mjs gear-api",
"bump:gear-idea": "node scripts/update-gear-idea-version.mjs",
"check:gear-idea-version": "node scripts/check-gear-idea-version.mjs"
"check:gear-idea-version": "node scripts/check-gear-idea-version.mjs",
"bundled-metadata:fetch": "node scripts/fetch-bundled-metadata.mjs",
"bundled-metadata:test": "node --test scripts/test-bundled-metadata.mjs",
"bundled-metadata:profile": "node scripts/profile-cold-start.mjs"
},
"devDependencies": {
"@babel/cli": "^7.28.6",
Expand All @@ -93,6 +97,7 @@
"@babel/preset-env": "^7.29.3",
"@babel/preset-typescript": "^7.28.5",
"@biomejs/biome": "^2.4.14",
"@polkadot/api": "16.5.6",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
Expand Down
15 changes: 15 additions & 0 deletions scripts/bundled-metadata.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"targets": [
{
"name": "vara-mainnet",
"rpc": "wss://rpc.vara.network",
"expectedGenesis": "0xfe1b4c55fd4d668101126434206571a7838a8b6b93a6d1b95d607e78e6c53763"
},
{
"name": "vara-testnet",
"rpc": "wss://testnet.vara.network",
"expectedGenesis": "0x525639f713f397dcf839bd022cd821f367ebcf179de7b9253531f8adbe5436d6"
}
],
"outputs": ["utils/bundled-metadata/src/data.ts"]
}
16 changes: 16 additions & 0 deletions scripts/common.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import * as path from 'node:path';

export const ROOT_DIR = path.resolve(import.meta.dirname, '../');

export async function safeDisconnect(...resources) {
for (const r of resources) {
try {
await r?.disconnect();
} catch {}
}
}

export function withTimeout(promise, label, timeoutMs) {
let timer;
const timeout = new Promise((_, rej) => {
timer = setTimeout(() => rej(new Error(`${label}: timed out after ${timeoutMs}ms`)), timeoutMs);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
Loading