@hugomrdias/foxer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.d.ts.map +1 -0
- package/dist/src/api/index.js +2 -0
- package/dist/src/api/index.js.map +1 -0
- package/dist/src/api/runner.d.ts +12 -0
- package/dist/src/api/runner.d.ts.map +1 -0
- package/dist/src/api/runner.js +29 -0
- package/dist/src/api/runner.js.map +1 -0
- package/dist/src/api/server.d.ts +18 -0
- package/dist/src/api/server.d.ts.map +1 -0
- package/dist/src/api/server.js +21 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/api/sql-middleware.d.ts +7 -0
- package/dist/src/api/sql-middleware.d.ts.map +1 -0
- package/dist/src/api/sql-middleware.js +95 -0
- package/dist/src/api/sql-middleware.js.map +1 -0
- package/dist/src/api/sql.d.ts +14 -0
- package/dist/src/api/sql.d.ts.map +1 -0
- package/dist/src/api/sql.js +108 -0
- package/dist/src/api/sql.js.map +1 -0
- package/dist/src/api/sse.d.ts +3 -0
- package/dist/src/api/sse.d.ts.map +1 -0
- package/dist/src/api/sse.js +11 -0
- package/dist/src/api/sse.js.map +1 -0
- package/dist/src/bin/dev.d.ts +3 -0
- package/dist/src/bin/dev.d.ts.map +1 -0
- package/dist/src/bin/dev.js +76 -0
- package/dist/src/bin/dev.js.map +1 -0
- package/dist/src/bin/flags.d.ts +32 -0
- package/dist/src/bin/flags.d.ts.map +1 -0
- package/dist/src/bin/flags.js +55 -0
- package/dist/src/bin/flags.js.map +1 -0
- package/dist/src/bin/index.d.ts +3 -0
- package/dist/src/bin/index.d.ts.map +1 -0
- package/dist/src/bin/index.js +22 -0
- package/dist/src/bin/index.js.map +1 -0
- package/dist/src/bin/utils.d.ts +4 -0
- package/dist/src/bin/utils.d.ts.map +1 -0
- package/dist/src/bin/utils.js +52 -0
- package/dist/src/bin/utils.js.map +1 -0
- package/dist/src/client/index.d.ts +18 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +150 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/config/config.d.ts +157 -0
- package/dist/src/config/config.d.ts.map +1 -0
- package/dist/src/config/config.js +65 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/config/env.d.ts +25 -0
- package/dist/src/config/env.d.ts.map +1 -0
- package/dist/src/config/env.js +21 -0
- package/dist/src/config/env.js.map +1 -0
- package/dist/src/contants.d.ts +4 -0
- package/dist/src/contants.d.ts.map +1 -0
- package/dist/src/contants.js +4 -0
- package/dist/src/contants.js.map +1 -0
- package/dist/src/db/actions/blocks.d.ts +44 -0
- package/dist/src/db/actions/blocks.d.ts.map +1 -0
- package/dist/src/db/actions/blocks.js +152 -0
- package/dist/src/db/actions/blocks.js.map +1 -0
- package/dist/src/db/actions/index.d.ts +2 -0
- package/dist/src/db/actions/index.d.ts.map +1 -0
- package/dist/src/db/actions/index.js +2 -0
- package/dist/src/db/actions/index.js.map +1 -0
- package/dist/src/db/actions/transactions.d.ts +11 -0
- package/dist/src/db/actions/transactions.d.ts.map +1 -0
- package/dist/src/db/actions/transactions.js +22 -0
- package/dist/src/db/actions/transactions.js.map +1 -0
- package/dist/src/db/client.d.ts +329 -0
- package/dist/src/db/client.d.ts.map +1 -0
- package/dist/src/db/client.js +108 -0
- package/dist/src/db/client.js.map +1 -0
- package/dist/src/db/column-types.d.ts +132 -0
- package/dist/src/db/column-types.d.ts.map +1 -0
- package/dist/src/db/column-types.js +86 -0
- package/dist/src/db/column-types.js.map +1 -0
- package/dist/src/db/encode.d.ts +10 -0
- package/dist/src/db/encode.d.ts.map +1 -0
- package/dist/src/db/encode.js +79 -0
- package/dist/src/db/encode.js.map +1 -0
- package/dist/src/db/migrate.d.ts +31 -0
- package/dist/src/db/migrate.d.ts.map +1 -0
- package/dist/src/db/migrate.js +147 -0
- package/dist/src/db/migrate.js.map +1 -0
- package/dist/src/db/schema/blocks.d.ts +369 -0
- package/dist/src/db/schema/blocks.d.ts.map +1 -0
- package/dist/src/db/schema/blocks.js +24 -0
- package/dist/src/db/schema/blocks.js.map +1 -0
- package/dist/src/db/schema/index.d.ts +1415 -0
- package/dist/src/db/schema/index.d.ts.map +1 -0
- package/dist/src/db/schema/index.js +18 -0
- package/dist/src/db/schema/index.js.map +1 -0
- package/dist/src/db/schema/transactions.d.ts +336 -0
- package/dist/src/db/schema/transactions.d.ts.map +1 -0
- package/dist/src/db/schema/transactions.js +33 -0
- package/dist/src/db/schema/transactions.js.map +1 -0
- package/dist/src/db/transaction.d.ts +7 -0
- package/dist/src/db/transaction.d.ts.map +1 -0
- package/dist/src/db/transaction.js +8 -0
- package/dist/src/db/transaction.js.map +1 -0
- package/dist/src/hooks/default-hooks.d.ts +2 -0
- package/dist/src/hooks/default-hooks.d.ts.map +1 -0
- package/dist/src/hooks/default-hooks.js +107 -0
- package/dist/src/hooks/default-hooks.js.map +1 -0
- package/dist/src/hooks/registry.d.ts +51 -0
- package/dist/src/hooks/registry.d.ts.map +1 -0
- package/dist/src/hooks/registry.js +32 -0
- package/dist/src/hooks/registry.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/indexer/backfill.d.ts +15 -0
- package/dist/src/indexer/backfill.d.ts.map +1 -0
- package/dist/src/indexer/backfill.js +95 -0
- package/dist/src/indexer/backfill.js.map +1 -0
- package/dist/src/indexer/live.d.ts +20 -0
- package/dist/src/indexer/live.d.ts.map +1 -0
- package/dist/src/indexer/live.js +51 -0
- package/dist/src/indexer/live.js.map +1 -0
- package/dist/src/indexer/process-block.d.ts +29 -0
- package/dist/src/indexer/process-block.d.ts.map +1 -0
- package/dist/src/indexer/process-block.js +91 -0
- package/dist/src/indexer/process-block.js.map +1 -0
- package/dist/src/indexer/queue-block.d.ts +18 -0
- package/dist/src/indexer/queue-block.d.ts.map +1 -0
- package/dist/src/indexer/queue-block.js +38 -0
- package/dist/src/indexer/queue-block.js.map +1 -0
- package/dist/src/indexer/reorg.d.ts +24 -0
- package/dist/src/indexer/reorg.d.ts.map +1 -0
- package/dist/src/indexer/reorg.js +83 -0
- package/dist/src/indexer/reorg.js.map +1 -0
- package/dist/src/indexer/runner.d.ts +14 -0
- package/dist/src/indexer/runner.d.ts.map +1 -0
- package/dist/src/indexer/runner.js +22 -0
- package/dist/src/indexer/runner.js.map +1 -0
- package/dist/src/rpc/client.d.ts +11 -0
- package/dist/src/rpc/client.d.ts.map +1 -0
- package/dist/src/rpc/client.js +18 -0
- package/dist/src/rpc/client.js.map +1 -0
- package/dist/src/rpc/get-block.d.ts +16 -0
- package/dist/src/rpc/get-block.d.ts.map +1 -0
- package/dist/src/rpc/get-block.js +77 -0
- package/dist/src/rpc/get-block.js.map +1 -0
- package/dist/src/rpc/get-logs.d.ts +11 -0
- package/dist/src/rpc/get-logs.d.ts.map +1 -0
- package/dist/src/rpc/get-logs.js +23 -0
- package/dist/src/rpc/get-logs.js.map +1 -0
- package/dist/src/schema.d.ts +3 -0
- package/dist/src/schema.d.ts.map +1 -0
- package/dist/src/schema.js +9 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/bloom.d.ts +6 -0
- package/dist/src/utils/bloom.d.ts.map +1 -0
- package/dist/src/utils/bloom.js +30 -0
- package/dist/src/utils/bloom.js.map +1 -0
- package/dist/src/utils/build-conflict-columns.d.ts +4 -0
- package/dist/src/utils/build-conflict-columns.d.ts.map +1 -0
- package/dist/src/utils/build-conflict-columns.js +14 -0
- package/dist/src/utils/build-conflict-columns.js.map +1 -0
- package/dist/src/utils/common.d.ts +2 -0
- package/dist/src/utils/common.d.ts.map +1 -0
- package/dist/src/utils/common.js +4 -0
- package/dist/src/utils/common.js.map +1 -0
- package/dist/src/utils/cursor.d.ts +5 -0
- package/dist/src/utils/cursor.d.ts.map +1 -0
- package/dist/src/utils/cursor.js +8 -0
- package/dist/src/utils/cursor.js.map +1 -0
- package/dist/src/utils/format.d.ts +2 -0
- package/dist/src/utils/format.d.ts.map +1 -0
- package/dist/src/utils/format.js +16 -0
- package/dist/src/utils/format.js.map +1 -0
- package/dist/src/utils/hash.d.ts +9 -0
- package/dist/src/utils/hash.d.ts.map +1 -0
- package/dist/src/utils/hash.js +15 -0
- package/dist/src/utils/hash.js.map +1 -0
- package/dist/src/utils/json.d.ts +5 -0
- package/dist/src/utils/json.d.ts.map +1 -0
- package/dist/src/utils/json.js +11 -0
- package/dist/src/utils/json.js.map +1 -0
- package/dist/src/utils/logger.d.ts +11 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +111 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/shutdown.d.ts +9 -0
- package/dist/src/utils/shutdown.d.ts.map +1 -0
- package/dist/src/utils/shutdown.js +24 -0
- package/dist/src/utils/shutdown.js.map +1 -0
- package/dist/src/utils/timer.d.ts +6 -0
- package/dist/src/utils/timer.d.ts.map +1 -0
- package/dist/src/utils/timer.js +9 -0
- package/dist/src/utils/timer.js.map +1 -0
- package/dist/src/utils/types.d.ts +39 -0
- package/dist/src/utils/types.d.ts.map +1 -0
- package/dist/src/utils/types.js +1 -0
- package/dist/src/utils/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/hello/apps/foc-api/README.md +69 -0
- package/hello/apps/foc-api/biome.json +8 -0
- package/hello/apps/foc-api/index.html +13 -0
- package/hello/apps/foc-api/package.json +39 -0
- package/hello/apps/foc-api/public/vite.svg +1 -0
- package/hello/apps/foc-api/src/app.css +45 -0
- package/hello/apps/foc-api/src/app.tsx +43 -0
- package/hello/apps/foc-api/src/assets/Cloudflare_Logo.svg +51 -0
- package/hello/apps/foc-api/src/assets/react.svg +1 -0
- package/hello/apps/foc-api/src/client.ts +41 -0
- package/hello/apps/foc-api/src/components/account.tsx +100 -0
- package/hello/apps/foc-api/src/components/wallet-options.tsx +43 -0
- package/hello/apps/foc-api/src/index.css +68 -0
- package/hello/apps/foc-api/src/main.tsx +38 -0
- package/hello/apps/foc-api/src/vite-env.d.ts +1 -0
- package/hello/apps/foc-api/tsconfig.app.json +44 -0
- package/hello/apps/foc-api/tsconfig.json +17 -0
- package/hello/apps/foc-api/tsconfig.node.json +25 -0
- package/hello/apps/foc-api/tsconfig.worker.json +8 -0
- package/hello/apps/foc-api/vite.config.ts +8 -0
- package/hello/apps/foc-api/worker/capabilities.ts +25 -0
- package/hello/apps/foc-api/worker/index.ts +64 -0
- package/hello/apps/foc-api/worker/router.ts +35 -0
- package/hello/apps/foc-api/worker-configuration.d.ts +7357 -0
- package/hello/apps/foc-api/wrangler.jsonc +50 -0
- package/hello/apps/foc-app/README.md +69 -0
- package/hello/apps/foc-app/biome.json +8 -0
- package/hello/apps/foc-app/index.html +13 -0
- package/hello/apps/foc-app/package.json +39 -0
- package/hello/apps/foc-app/public/vite.svg +1 -0
- package/hello/apps/foc-app/src/app.css +45 -0
- package/hello/apps/foc-app/src/app.tsx +43 -0
- package/hello/apps/foc-app/src/assets/Cloudflare_Logo.svg +51 -0
- package/hello/apps/foc-app/src/assets/react.svg +1 -0
- package/hello/apps/foc-app/src/client.ts +41 -0
- package/hello/apps/foc-app/src/components/account.tsx +100 -0
- package/hello/apps/foc-app/src/components/wallet-options.tsx +43 -0
- package/hello/apps/foc-app/src/index.css +68 -0
- package/hello/apps/foc-app/src/main.tsx +38 -0
- package/hello/apps/foc-app/src/vite-env.d.ts +1 -0
- package/hello/apps/foc-app/tsconfig.app.json +44 -0
- package/hello/apps/foc-app/tsconfig.json +17 -0
- package/hello/apps/foc-app/tsconfig.node.json +25 -0
- package/hello/apps/foc-app/tsconfig.worker.json +8 -0
- package/hello/apps/foc-app/vite.config.ts +8 -0
- package/hello/apps/foc-app/worker/capabilities.ts +25 -0
- package/hello/apps/foc-app/worker/index.ts +64 -0
- package/hello/apps/foc-app/worker/router.ts +35 -0
- package/hello/apps/foc-app/worker-configuration.d.ts +7357 -0
- package/hello/apps/foc-app/wrangler.jsonc +50 -0
- package/hello/biome.json +50 -0
- package/hello/package.json +22 -0
- package/hello/pnpm-workspace.yaml +3 -0
- package/hello/tsconfig.json +37 -0
- package/package.json +78 -0
- package/src/api/index.ts +1 -0
- package/src/api/runner.ts +43 -0
- package/src/api/server.ts +38 -0
- package/src/api/sql-middleware.ts +131 -0
- package/src/api/sql.ts +149 -0
- package/src/api/sse.ts +12 -0
- package/src/bin/create.ts +199 -0
- package/src/bin/dev.ts +91 -0
- package/src/bin/flags.ts +65 -0
- package/src/bin/index.ts +28 -0
- package/src/bin/utils.ts +55 -0
- package/src/config/config.ts +221 -0
- package/src/config/env.ts +28 -0
- package/src/contants.ts +3 -0
- package/src/db/actions/blocks.ts +209 -0
- package/src/db/actions/index.ts +1 -0
- package/src/db/actions/transactions.ts +32 -0
- package/src/db/client.ts +186 -0
- package/src/db/column-types.ts +105 -0
- package/src/db/encode.ts +99 -0
- package/src/db/migrate.ts +222 -0
- package/src/db/schema/blocks.ts +24 -0
- package/src/db/schema/index.ts +21 -0
- package/src/db/schema/transactions.ts +39 -0
- package/src/db/transaction.ts +20 -0
- package/src/hooks/registry.ts +107 -0
- package/src/index.ts +9 -0
- package/src/indexer/backfill.ts +133 -0
- package/src/indexer/live.ts +76 -0
- package/src/indexer/process-block.ts +142 -0
- package/src/indexer/queue-block.ts +74 -0
- package/src/indexer/reorg.ts +120 -0
- package/src/indexer/runner.ts +35 -0
- package/src/rpc/client.ts +27 -0
- package/src/rpc/get-block.ts +100 -0
- package/src/rpc/get-logs.ts +38 -0
- package/src/schema.ts +10 -0
- package/src/types.ts +32 -0
- package/src/utils/bloom.ts +41 -0
- package/src/utils/build-conflict-columns.ts +26 -0
- package/src/utils/common.ts +3 -0
- package/src/utils/cursor.ts +7 -0
- package/src/utils/format.ts +18 -0
- package/src/utils/hash.ts +17 -0
- package/src/utils/json.ts +11 -0
- package/src/utils/logger.ts +149 -0
- package/src/utils/shutdown.ts +36 -0
- package/src/utils/timer.ts +8 -0
- package/src/utils/types.ts +87 -0
- package/template/biome.json +50 -0
- package/template/package.json +22 -0
- package/template/pnpm-workspace.yaml +3 -0
- package/template/tsconfig.json +37 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { AbiEvent, Log, PublicClient } from 'viem'
|
|
2
|
+
import type { FilteredContracts, InternalConfig } from '../config/config.ts'
|
|
3
|
+
import { cacheBlockAndTransactions } from '../db/actions/blocks.ts'
|
|
4
|
+
import type { Database } from '../db/client.ts'
|
|
5
|
+
import type { relations, schema } from '../db/schema/index.ts'
|
|
6
|
+
import { withTransaction } from '../db/transaction.ts'
|
|
7
|
+
import type { HookRegistry } from '../hooks/registry.ts'
|
|
8
|
+
import { safeGetBlock } from '../rpc/get-block.ts'
|
|
9
|
+
import type { EncodedBlockWithTransactions, EncodedTransaction } from '../types'
|
|
10
|
+
import type { Logger } from '../utils/logger.ts'
|
|
11
|
+
import { ensureParentContinuity } from './reorg.ts'
|
|
12
|
+
|
|
13
|
+
export type ProcessBlockResult =
|
|
14
|
+
| { status: 'processed' }
|
|
15
|
+
| { status: 'reorg'; rewindTo: bigint }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Processes one block: continuity check, event writes, and optional cursor update.
|
|
19
|
+
*/
|
|
20
|
+
export async function processBlock(args: {
|
|
21
|
+
logger: Logger
|
|
22
|
+
config: InternalConfig
|
|
23
|
+
db: Database<typeof schema, typeof relations>
|
|
24
|
+
client: PublicClient
|
|
25
|
+
registry: HookRegistry<NonNullable<unknown>>
|
|
26
|
+
blockNumber: bigint
|
|
27
|
+
logs?: Log<bigint, number, false, AbiEvent>[]
|
|
28
|
+
block?: EncodedBlockWithTransactions
|
|
29
|
+
type: 'backfill' | 'live'
|
|
30
|
+
contracts: FilteredContracts
|
|
31
|
+
}): Promise<ProcessBlockResult> {
|
|
32
|
+
const {
|
|
33
|
+
logger,
|
|
34
|
+
config,
|
|
35
|
+
db,
|
|
36
|
+
client,
|
|
37
|
+
registry,
|
|
38
|
+
blockNumber,
|
|
39
|
+
block: prefetchedBlock,
|
|
40
|
+
logs: prefetchedLogs,
|
|
41
|
+
type,
|
|
42
|
+
contracts,
|
|
43
|
+
} = args
|
|
44
|
+
const transactionByHash = new Map<`0x${string}`, EncodedTransaction>()
|
|
45
|
+
|
|
46
|
+
let block: EncodedBlockWithTransactions | undefined
|
|
47
|
+
let logs: Log<bigint, number, false, AbiEvent>[] | undefined
|
|
48
|
+
|
|
49
|
+
if (prefetchedBlock) {
|
|
50
|
+
block = prefetchedBlock
|
|
51
|
+
}
|
|
52
|
+
if (prefetchedLogs) {
|
|
53
|
+
logs = prefetchedLogs
|
|
54
|
+
}
|
|
55
|
+
if (!block || !logs) {
|
|
56
|
+
const [blockResult, logsResult] = await Promise.all([
|
|
57
|
+
safeGetBlock({ client, blockNumber, db }),
|
|
58
|
+
client.getLogs({
|
|
59
|
+
address: contracts.addresses,
|
|
60
|
+
events: contracts.eventAbis,
|
|
61
|
+
fromBlock: blockNumber,
|
|
62
|
+
toBlock: blockNumber,
|
|
63
|
+
}),
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
block = blockResult
|
|
67
|
+
logs = logsResult
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const tx of block.transactions) {
|
|
71
|
+
transactionByHash.set(tx.hash, tx)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (type === 'live') {
|
|
75
|
+
const rewindTo = await ensureParentContinuity({
|
|
76
|
+
logger,
|
|
77
|
+
db,
|
|
78
|
+
client,
|
|
79
|
+
block,
|
|
80
|
+
})
|
|
81
|
+
if (rewindTo != null) {
|
|
82
|
+
return { status: 'reorg', rewindTo }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const write = async (tx: Database<typeof schema, typeof relations>) => {
|
|
87
|
+
if (type === 'live') {
|
|
88
|
+
await cacheBlockAndTransactions({
|
|
89
|
+
db: tx,
|
|
90
|
+
block,
|
|
91
|
+
logger,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const log of logs) {
|
|
96
|
+
const contractName = contracts.contractNameByAddress[log.address]
|
|
97
|
+
|
|
98
|
+
if (!contractName) {
|
|
99
|
+
logger.trace(
|
|
100
|
+
{ address: log.address },
|
|
101
|
+
'contract not found in contract name by address'
|
|
102
|
+
)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const eventName = log.eventName
|
|
106
|
+
|
|
107
|
+
if (!contracts.eventNames.has(eventName)) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const transaction = transactionByHash.get(log.transactionHash)
|
|
111
|
+
|
|
112
|
+
if (!transaction) {
|
|
113
|
+
logger.trace(
|
|
114
|
+
{ transactionHash: log.transactionHash },
|
|
115
|
+
|
|
116
|
+
'transaction not found in block transaction list'
|
|
117
|
+
)
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
await registry.dispatch({
|
|
121
|
+
key: `${contractName}:${eventName}` as never,
|
|
122
|
+
args: log.args as never,
|
|
123
|
+
log: log as never,
|
|
124
|
+
block,
|
|
125
|
+
transaction,
|
|
126
|
+
context: {
|
|
127
|
+
db: tx,
|
|
128
|
+
chainId: config.client.chain.id,
|
|
129
|
+
logger,
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (type === 'backfill') {
|
|
136
|
+
await write(db)
|
|
137
|
+
} else {
|
|
138
|
+
await withTransaction(db, write)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { status: 'processed' }
|
|
142
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Logger } from 'pino'
|
|
2
|
+
import type { PublicClient } from 'viem'
|
|
3
|
+
import { filterContracts, type InternalConfig } from '../config/config.ts'
|
|
4
|
+
import type { Database } from '../db/client.ts'
|
|
5
|
+
import type { relations, schema } from '../db/schema/index.ts'
|
|
6
|
+
import type { HookRegistry } from '../hooks/registry.ts'
|
|
7
|
+
import { startClock } from '../utils/timer.ts'
|
|
8
|
+
import { processBlock } from './process-block.ts'
|
|
9
|
+
|
|
10
|
+
export type QueueBlockArgs = {
|
|
11
|
+
logger: Logger
|
|
12
|
+
blockNumber: bigint
|
|
13
|
+
onRewind: (rewindTo: bigint) => void
|
|
14
|
+
config: InternalConfig
|
|
15
|
+
db: Database<typeof schema, typeof relations>
|
|
16
|
+
client: PublicClient
|
|
17
|
+
registry: HookRegistry
|
|
18
|
+
queueSize: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function queueBlock(args: QueueBlockArgs): Promise<void> {
|
|
22
|
+
const {
|
|
23
|
+
config,
|
|
24
|
+
db,
|
|
25
|
+
client,
|
|
26
|
+
registry,
|
|
27
|
+
blockNumber,
|
|
28
|
+
logger,
|
|
29
|
+
onRewind,
|
|
30
|
+
queueSize,
|
|
31
|
+
} = args
|
|
32
|
+
|
|
33
|
+
const endClock = startClock()
|
|
34
|
+
try {
|
|
35
|
+
const contracts = filterContracts(config, blockNumber, blockNumber)
|
|
36
|
+
const result = await processBlock({
|
|
37
|
+
logger,
|
|
38
|
+
config,
|
|
39
|
+
db,
|
|
40
|
+
client,
|
|
41
|
+
registry,
|
|
42
|
+
blockNumber,
|
|
43
|
+
type: 'live',
|
|
44
|
+
contracts,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (result.status === 'reorg') {
|
|
48
|
+
logger.warn(
|
|
49
|
+
{
|
|
50
|
+
blockNumber: blockNumber.toString(),
|
|
51
|
+
rewindTo: result.rewindTo.toString(),
|
|
52
|
+
},
|
|
53
|
+
'reorg detected during live processing; rewinding'
|
|
54
|
+
)
|
|
55
|
+
onRewind(result.rewindTo)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
logger.info(
|
|
60
|
+
{
|
|
61
|
+
duration: endClock(),
|
|
62
|
+
blockNumber: blockNumber.toString(),
|
|
63
|
+
queueSize,
|
|
64
|
+
},
|
|
65
|
+
'processed live block'
|
|
66
|
+
)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error(
|
|
69
|
+
{ error, blockNumber: blockNumber.toString() },
|
|
70
|
+
'block processing failed; rewinding'
|
|
71
|
+
)
|
|
72
|
+
onRewind(blockNumber - 1n)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { PublicClient } from 'viem'
|
|
2
|
+
import { deleteBlocksFrom } from '../db/actions/blocks.ts'
|
|
3
|
+
import type { Database } from '../db/client.ts'
|
|
4
|
+
import { safeGetBlock } from '../rpc/get-block.ts'
|
|
5
|
+
import type { EncodedBlockWithTransactions } from '../types'
|
|
6
|
+
import { hashEquals } from '../utils/hash.ts'
|
|
7
|
+
import type { Logger } from '../utils/logger.ts'
|
|
8
|
+
import { startClock } from '../utils/timer.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Verifies parent-hash continuity and rolls back divergent canonical rows if needed.
|
|
12
|
+
* Returns the rewind start block when a reorg is detected.
|
|
13
|
+
*/
|
|
14
|
+
export async function ensureParentContinuity(args: {
|
|
15
|
+
logger: Logger
|
|
16
|
+
db: Database
|
|
17
|
+
client: PublicClient
|
|
18
|
+
block: EncodedBlockWithTransactions
|
|
19
|
+
}): Promise<bigint | null> {
|
|
20
|
+
const { logger, db, client, block } = args
|
|
21
|
+
if (block.number === 0n) return null
|
|
22
|
+
|
|
23
|
+
// get the previous block
|
|
24
|
+
const previous = (
|
|
25
|
+
await db.$prepared.getBlockById.execute({
|
|
26
|
+
db,
|
|
27
|
+
blockNumber: block.number - 1n,
|
|
28
|
+
})
|
|
29
|
+
)[0]
|
|
30
|
+
if (!previous) return null
|
|
31
|
+
|
|
32
|
+
// check if the previous block's hash is the same as the block's parent hash
|
|
33
|
+
if (hashEquals(previous.hash, block.parentHash)) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.warn(
|
|
38
|
+
{ blockNumber: block.number.toString() },
|
|
39
|
+
'parent mismatch detected; rolling back'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// Walk backward from the immediate parent of the failing block until we find
|
|
43
|
+
// a block number where DB and chain hashes agree again.
|
|
44
|
+
let cursor = block.number - 1n
|
|
45
|
+
|
|
46
|
+
while (true) {
|
|
47
|
+
// 1) Read the DB's canonical block at this height.
|
|
48
|
+
const dbBlock = (
|
|
49
|
+
await db.$prepared.getBlockById.execute({ blockNumber: cursor })
|
|
50
|
+
)[0]
|
|
51
|
+
if (!dbBlock) {
|
|
52
|
+
cursor -= 1n
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2) Read chain block at the same height.
|
|
57
|
+
const chainBlock = await safeGetBlock({ client, blockNumber: cursor, db })
|
|
58
|
+
// if (blockResult.status === 'null_round') {
|
|
59
|
+
// cursor -= 1n
|
|
60
|
+
// continue
|
|
61
|
+
// }
|
|
62
|
+
|
|
63
|
+
// 3) Found the last common ancestor. Rewind to the first divergent height.
|
|
64
|
+
if (hashEquals(chainBlock.hash, dbBlock.hash)) {
|
|
65
|
+
const rewindTo = cursor
|
|
66
|
+
await deleteBlocksFrom(db, rewindTo)
|
|
67
|
+
return rewindTo
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4) Still divergent at this height; keep scanning backward.
|
|
71
|
+
// TODO This should finality depth and it should delete and throw critical error
|
|
72
|
+
if (cursor === 0n) {
|
|
73
|
+
await deleteBlocksFrom(db, 0n)
|
|
74
|
+
return 0n
|
|
75
|
+
}
|
|
76
|
+
cursor -= 1n
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates recent indexed blocks against chain state on startup.
|
|
82
|
+
*/
|
|
83
|
+
export async function verifyRecentBlocks(args: {
|
|
84
|
+
logger: Logger
|
|
85
|
+
db: Database
|
|
86
|
+
client: PublicClient
|
|
87
|
+
depth: bigint
|
|
88
|
+
}): Promise<void> {
|
|
89
|
+
const { logger, db, client, depth } = args
|
|
90
|
+
const endClock = startClock()
|
|
91
|
+
const latest =
|
|
92
|
+
(await db.$prepared.getLatestBlock.execute())[0]?.number ?? null
|
|
93
|
+
|
|
94
|
+
if (latest == null) return
|
|
95
|
+
|
|
96
|
+
const start = latest - depth >= 0n ? latest - depth : 0n
|
|
97
|
+
let blockNumber = start
|
|
98
|
+
while (blockNumber <= latest) {
|
|
99
|
+
const dbBlock = (
|
|
100
|
+
await db.$prepared.getBlockById.execute({ blockNumber })
|
|
101
|
+
)[0]
|
|
102
|
+
|
|
103
|
+
if (!dbBlock) {
|
|
104
|
+
blockNumber += 1n
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
const chainBlock = await safeGetBlock({ client, blockNumber, db })
|
|
108
|
+
|
|
109
|
+
if (!hashEquals(chainBlock.hash, dbBlock.hash)) {
|
|
110
|
+
logger.warn(
|
|
111
|
+
{ blockNumber: blockNumber.toString() },
|
|
112
|
+
'startup sanity check mismatch detected'
|
|
113
|
+
)
|
|
114
|
+
await deleteBlocksFrom(db, blockNumber)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
blockNumber += 1n
|
|
118
|
+
}
|
|
119
|
+
logger.info({ duration: endClock() }, 'startup sanity check completed')
|
|
120
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { InternalConfig } from '../config/config.ts'
|
|
2
|
+
import type { Database } from '../db/client.ts'
|
|
3
|
+
import type { relations, schema } from '../db/schema/index.ts'
|
|
4
|
+
import type { HookRegistry } from '../hooks/registry.ts'
|
|
5
|
+
import type { Logger } from '../utils/logger.ts'
|
|
6
|
+
import { runBackfill } from './backfill.ts'
|
|
7
|
+
import { startLiveSync } from './live.ts'
|
|
8
|
+
import { verifyRecentBlocks } from './reorg.ts'
|
|
9
|
+
|
|
10
|
+
export async function bootstrapIndexer(options: {
|
|
11
|
+
logger: Logger
|
|
12
|
+
db: Database<typeof schema, typeof relations>
|
|
13
|
+
registry: HookRegistry
|
|
14
|
+
config: InternalConfig
|
|
15
|
+
}): Promise<{ stop: () => void }> {
|
|
16
|
+
await verifyRecentBlocks({
|
|
17
|
+
logger: options.logger,
|
|
18
|
+
db: options.db,
|
|
19
|
+
client: options.config.clients.backfill,
|
|
20
|
+
depth: options.config.finality,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const nextCursor = await runBackfill(options)
|
|
24
|
+
|
|
25
|
+
const live = startLiveSync({
|
|
26
|
+
logger: options.logger,
|
|
27
|
+
config: options.config,
|
|
28
|
+
db: options.db,
|
|
29
|
+
client: options.config.clients.live,
|
|
30
|
+
registry: options.registry,
|
|
31
|
+
initialCursor: nextCursor,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return live
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createPublicClient, type PublicClient } from 'viem'
|
|
2
|
+
import type { ClientConfig } from '../config/config.ts'
|
|
3
|
+
|
|
4
|
+
export type RpcClients = {
|
|
5
|
+
backfill: PublicClient
|
|
6
|
+
live: PublicClient
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a viem public client configured for the target FEVM chain.
|
|
11
|
+
*/
|
|
12
|
+
export function createRpcClients(options: ClientConfig): RpcClients {
|
|
13
|
+
const backfill = createPublicClient(options)
|
|
14
|
+
|
|
15
|
+
const liveTransport = options.realtimeTransport ?? options.transport
|
|
16
|
+
|
|
17
|
+
const live = createPublicClient({
|
|
18
|
+
chain: options.chain,
|
|
19
|
+
transport: liveTransport,
|
|
20
|
+
pollingInterval: 1000,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
backfill,
|
|
25
|
+
live,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Hash, PublicClient } from 'viem'
|
|
2
|
+
import type { Database } from '../db/client.ts'
|
|
3
|
+
import {
|
|
4
|
+
encodeBlockWithTransactions,
|
|
5
|
+
encodeNullRoundBlock,
|
|
6
|
+
} from '../db/encode.ts'
|
|
7
|
+
import type { EncodedBlockWithTransactions } from '../types.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetches a block while normalizing null-round behavior into an explicit result.
|
|
11
|
+
*/
|
|
12
|
+
export async function safeGetBlock(options: {
|
|
13
|
+
client: PublicClient
|
|
14
|
+
blockNumber: bigint
|
|
15
|
+
db: Database
|
|
16
|
+
}): Promise<EncodedBlockWithTransactions> {
|
|
17
|
+
const { client, blockNumber, db } = options
|
|
18
|
+
try {
|
|
19
|
+
const block = await client.getBlock({
|
|
20
|
+
blockNumber,
|
|
21
|
+
includeTransactions: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return encodeBlockWithTransactions(block)
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (isNullRoundRpcError(error)) {
|
|
27
|
+
let previousBlock:
|
|
28
|
+
| { number: bigint; hash: Hash; parentHash: Hash }
|
|
29
|
+
| undefined
|
|
30
|
+
|
|
31
|
+
previousBlock = (
|
|
32
|
+
await db.$prepared.getBlockById.execute({
|
|
33
|
+
blockNumber: blockNumber - 1n,
|
|
34
|
+
})
|
|
35
|
+
)[0]
|
|
36
|
+
|
|
37
|
+
let previousBlockNumber = blockNumber - 1n
|
|
38
|
+
// go to the chain and loop back until a full block is found
|
|
39
|
+
if (!previousBlock) {
|
|
40
|
+
while (!previousBlock) {
|
|
41
|
+
try {
|
|
42
|
+
const block = await client.getBlock({
|
|
43
|
+
blockNumber: previousBlockNumber,
|
|
44
|
+
})
|
|
45
|
+
previousBlock = {
|
|
46
|
+
number: block.number,
|
|
47
|
+
hash: block.hash,
|
|
48
|
+
parentHash: block.parentHash,
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// catched another null round, keep going
|
|
52
|
+
if (isNullRoundRpcError(error)) {
|
|
53
|
+
previousBlockNumber -= 1n
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
throw error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return encodeNullRoundBlock({
|
|
61
|
+
number: blockNumber,
|
|
62
|
+
hash: previousBlock.hash,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detects Filecoin null-round RPC errors so callers can skip non-existent rounds.
|
|
71
|
+
*/
|
|
72
|
+
export function isNullRoundRpcError(error: unknown): boolean {
|
|
73
|
+
if (!(error instanceof Error)) return false
|
|
74
|
+
|
|
75
|
+
const message = error.message.toLowerCase()
|
|
76
|
+
if (message.includes('null round')) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const details = (error as { details?: unknown }).details
|
|
81
|
+
if (
|
|
82
|
+
typeof details === 'string' &&
|
|
83
|
+
details.toLowerCase().includes('null round')
|
|
84
|
+
) {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const cause = (error as { cause?: unknown }).cause
|
|
89
|
+
if (cause && typeof cause === 'object') {
|
|
90
|
+
const causeMessage = (cause as { message?: unknown }).message
|
|
91
|
+
if (
|
|
92
|
+
typeof causeMessage === 'string' &&
|
|
93
|
+
causeMessage.toLowerCase().includes('null round')
|
|
94
|
+
) {
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AbiEvent, Address, Log, PublicClient } from 'viem'
|
|
2
|
+
import type { Logger } from '../utils/logger.ts'
|
|
3
|
+
import { startClock } from '../utils/timer.ts'
|
|
4
|
+
|
|
5
|
+
export async function getLogsInRange(args: {
|
|
6
|
+
logger: Logger
|
|
7
|
+
client: PublicClient
|
|
8
|
+
addresses: Address[]
|
|
9
|
+
events: readonly AbiEvent[]
|
|
10
|
+
fromBlock: bigint
|
|
11
|
+
toBlock: bigint
|
|
12
|
+
}): Promise<Map<bigint, Log<bigint, number, false, AbiEvent>[]>> {
|
|
13
|
+
const { logger, client, addresses, events, fromBlock, toBlock } = args
|
|
14
|
+
|
|
15
|
+
const endClock = startClock()
|
|
16
|
+
|
|
17
|
+
const logsByBlock = new Map<bigint, Log<bigint, number, false, AbiEvent>[]>()
|
|
18
|
+
const logs = await client.getLogs({
|
|
19
|
+
address: addresses,
|
|
20
|
+
events: events,
|
|
21
|
+
fromBlock: fromBlock,
|
|
22
|
+
toBlock: toBlock,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
for (const log of logs) {
|
|
26
|
+
const byBlock = logsByBlock.get(log.blockNumber) ?? []
|
|
27
|
+
byBlock.push(log)
|
|
28
|
+
logsByBlock.set(log.blockNumber, byBlock)
|
|
29
|
+
}
|
|
30
|
+
logger.info(
|
|
31
|
+
{
|
|
32
|
+
logs: logsByBlock.size,
|
|
33
|
+
duration: endClock(),
|
|
34
|
+
},
|
|
35
|
+
'get logs'
|
|
36
|
+
)
|
|
37
|
+
return logsByBlock
|
|
38
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
5
|
+
|
|
6
|
+
export const schemaFiles = [
|
|
7
|
+
resolve(__dirname, './db/schema/blocks.js'),
|
|
8
|
+
resolve(__dirname, './db/schema/transactions.js'),
|
|
9
|
+
]
|
|
10
|
+
export { schema } from './db/schema/index.ts'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Simplify } from 'type-fest'
|
|
2
|
+
import type { Block, Transaction } from 'viem'
|
|
3
|
+
import type { Schema } from './db/schema/index'
|
|
4
|
+
/**
|
|
5
|
+
* Generic result with error
|
|
6
|
+
*/
|
|
7
|
+
export type MaybeResult<ResultType = unknown, ErrorType = Error> =
|
|
8
|
+
| {
|
|
9
|
+
error: ErrorType
|
|
10
|
+
result?: undefined
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
result: ResultType
|
|
14
|
+
error?: undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type UnknownObject = NonNullable<unknown>
|
|
18
|
+
|
|
19
|
+
export type ChainTransaction = Transaction<bigint, number, false>
|
|
20
|
+
export type ChainBlock = Block<
|
|
21
|
+
bigint,
|
|
22
|
+
true,
|
|
23
|
+
'latest' | 'safe' | 'finalized',
|
|
24
|
+
ChainTransaction
|
|
25
|
+
>
|
|
26
|
+
export type EncodedBlock = Schema['blocks']['$inferInsert']
|
|
27
|
+
export type EncodedTransaction = Schema['transactions']['$inferInsert']
|
|
28
|
+
export type EncodedBlockWithTransactions = Simplify<
|
|
29
|
+
EncodedBlock & {
|
|
30
|
+
transactions: EncodedTransaction[]
|
|
31
|
+
}
|
|
32
|
+
>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/noNonNullAssertion: no need to check for null */
|
|
2
|
+
import { type Hex, hexToBytes, keccak256 } from 'viem'
|
|
3
|
+
|
|
4
|
+
export const zeroLogsBloom =
|
|
5
|
+
'0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
|
6
|
+
|
|
7
|
+
// const BLOOM_SIZE_BYTES = 256;
|
|
8
|
+
|
|
9
|
+
export const isInBloom = (bloomHex: Hex, value: Hex): boolean => {
|
|
10
|
+
const bloom = hexToBytes(bloomHex)
|
|
11
|
+
const hash = hexToBytes(keccak256(value))
|
|
12
|
+
|
|
13
|
+
// Ethereum uses 3 pairs of bytes from the hash to determine 3 bits
|
|
14
|
+
for (let i = 0; i < 6; i += 2) {
|
|
15
|
+
// Calculate the bit index (0 to 2047)
|
|
16
|
+
const bitIndex = ((hash[i]! << 8) | hash[i + 1]!) & 0x7ff
|
|
17
|
+
|
|
18
|
+
// Check if that bit is set in the 256-byte bloom array
|
|
19
|
+
const byteIndex = 255 - Math.floor(bitIndex / 8)
|
|
20
|
+
const bitMask = 1 << (bitIndex % 8)
|
|
21
|
+
|
|
22
|
+
if ((bloom[byteIndex]! & bitMask) === 0) {
|
|
23
|
+
return false // Definitely NOT in this block
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true // PROBABLY in this block (could be a false positive)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isBlockInteresting(
|
|
30
|
+
bloomHex: Hex,
|
|
31
|
+
addresses: Hex[],
|
|
32
|
+
topics: Hex[]
|
|
33
|
+
): boolean {
|
|
34
|
+
// Check if ANY of our target contracts might be in this block
|
|
35
|
+
const hasContract = addresses.some((addr) => isInBloom(bloomHex, addr))
|
|
36
|
+
if (!hasContract) return false
|
|
37
|
+
|
|
38
|
+
// Check if ANY of our target event signatures might be in this block
|
|
39
|
+
const hasEvent = topics.some((topic) => isInBloom(bloomHex, topic))
|
|
40
|
+
return hasEvent
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type SQL, sql } from 'drizzle-orm'
|
|
2
|
+
import type { PgTable } from 'drizzle-orm/pg-core'
|
|
3
|
+
import { getColumns } from 'drizzle-orm/utils'
|
|
4
|
+
import { snakeCase } from 'scule'
|
|
5
|
+
|
|
6
|
+
export const buildConflictUpdateColumns = <
|
|
7
|
+
T extends PgTable,
|
|
8
|
+
Q extends keyof T['_']['columns'],
|
|
9
|
+
>(
|
|
10
|
+
table: T,
|
|
11
|
+
columns?: Q[]
|
|
12
|
+
) => {
|
|
13
|
+
const cls = getColumns(table)
|
|
14
|
+
const cols = columns ?? (Object.keys(cls) as Q[])
|
|
15
|
+
const r = cols.reduce(
|
|
16
|
+
(acc, column) => {
|
|
17
|
+
const colName = snakeCase(cls[column].name)
|
|
18
|
+
|
|
19
|
+
acc[column] = sql.raw(`excluded.${colName}`)
|
|
20
|
+
return acc
|
|
21
|
+
},
|
|
22
|
+
{} as Record<Q, SQL>
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return r
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const formatLogDuration = (ms: number) => {
|
|
2
|
+
// If less than 1 second, return ms.
|
|
3
|
+
if (ms < 1000) return `${Math.round(ms)}ms`
|
|
4
|
+
const seconds = Math.floor(ms / 1000)
|
|
5
|
+
|
|
6
|
+
const h = Math.floor(seconds / 3600)
|
|
7
|
+
const m = Math.floor((seconds - h * 3600) / 60)
|
|
8
|
+
const s = seconds - h * 3600 - m * 60
|
|
9
|
+
const secWithMs = ((ms % 60000) / 1000).toFixed(3).replace(/\.?0+$/, '') // seconds including ms fraction, max 59.999
|
|
10
|
+
|
|
11
|
+
const hstr = h > 0 ? `${h}h ` : ''
|
|
12
|
+
const mstr = m > 0 || h > 0 ? `${m < 10 && h > 0 ? '0' : ''}${m}m ` : ''
|
|
13
|
+
// Add milliseconds fraction to the seconds string, e.g., 2.322s
|
|
14
|
+
const sstr =
|
|
15
|
+
s > 0 || m > 0 ? `${s < 10 && m > 0 ? '0' : ''}${secWithMs}s` : ''
|
|
16
|
+
|
|
17
|
+
return `${hstr}${mstr}${sstr}`
|
|
18
|
+
}
|