@latticexyz/store-indexer 2.0.0-next.14 → 2.0.0-next.15

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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import{b as c,c as m}from"../chunk-2E5MDUA2.js";import"dotenv/config";import{z as s}from"zod";import{eq as T}from"drizzle-orm";import{createPublicClient as b,fallback as C,webSocket as E,http as g}from"viem";import{isDefined as _}from"@latticexyz/common/utils";import{combineLatest as A,filter as L,first as R}from"rxjs";import{drizzle as h}from"drizzle-orm/postgres-js";import S from"postgres";import{createStorageAdapter as k}from"@latticexyz/store-sync/postgres-decoded";import{createStoreSync as w}from"@latticexyz/store-sync";var e=m(s.intersection(c,s.object({DATABASE_URL:s.string(),HEALTHCHECK_HOST:s.string().optional(),HEALTHCHECK_PORT:s.coerce.number().optional()}))),O=[e.RPC_WS_URL?E(e.RPC_WS_URL):void 0,e.RPC_HTTP_URL?g(e.RPC_HTTP_URL):void 0].filter(_),l=b({transport:C(O),pollingInterval:e.POLLING_INTERVAL}),P=await l.getChainId(),u=h(S(e.DATABASE_URL,{prepare:!1})),{storageAdapter:K,tables:p}=await k({database:u,publicClient:l}),i=e.START_BLOCK;try{let o=await u.select().from(p.configTable).where(T(p.configTable.chainId,P)).limit(1).execute().then(r=>r.find(()=>!0));o?.blockNumber!=null&&(i=o.blockNumber+1n,console.log("resuming from block number",i))}catch{}var{latestBlockNumber$:B,storedBlockLogs$:f}=await w({storageAdapter:K,publicClient:l,startBlock:i,maxBlockRange:e.MAX_BLOCK_RANGE,address:e.STORE_ADDRESS});f.subscribe();var H=!1;A([B,f]).pipe(L(([o,{blockNumber:r}])=>o===r),R()).subscribe(()=>{H=!0,console.log("all caught up")});if(e.HEALTHCHECK_HOST!=null||e.HEALTHCHECK_PORT!=null){let{default:o}=await import("koa"),{default:r}=await import("@koa/cors"),{default:d}=await import("@koa/router"),n=new o;n.use(r());let a=new d;a.get("/",t=>{t.body="emit HelloWorld();"}),a.get("/healthz",t=>{t.status=200}),a.get("/readyz",t=>{H?(t.status=200,t.body="ready"):(t.status=424,t.body="backfilling")}),n.use(a.routes()),n.use(a.allowedMethods()),n.listen({host:e.HEALTHCHECK_HOST,port:e.HEALTHCHECK_PORT}),console.log(`postgres indexer healthcheck server listening on http://${e.HEALTHCHECK_HOST}:${e.HEALTHCHECK_PORT}`)}
3
+ //# sourceMappingURL=postgres-decoded-indexer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../bin/postgres-decoded-indexer.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { z } from \"zod\";\nimport { eq } from \"drizzle-orm\";\nimport { createPublicClient, fallback, webSocket, http, Transport } from \"viem\";\nimport { isDefined } from \"@latticexyz/common/utils\";\nimport { combineLatest, filter, first } from \"rxjs\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { createStorageAdapter } from \"@latticexyz/store-sync/postgres-decoded\";\nimport { createStoreSync } from \"@latticexyz/store-sync\";\nimport { indexerEnvSchema, parseEnv } from \"./parseEnv\";\n\nconst env = parseEnv(\n z.intersection(\n indexerEnvSchema,\n z.object({\n DATABASE_URL: z.string(),\n HEALTHCHECK_HOST: z.string().optional(),\n HEALTHCHECK_PORT: z.coerce.number().optional(),\n })\n )\n);\n\nconst transports: Transport[] = [\n // prefer WS when specified\n env.RPC_WS_URL ? webSocket(env.RPC_WS_URL) : undefined,\n // otherwise use or fallback to HTTP\n env.RPC_HTTP_URL ? http(env.RPC_HTTP_URL) : undefined,\n].filter(isDefined);\n\nconst publicClient = createPublicClient({\n transport: fallback(transports),\n pollingInterval: env.POLLING_INTERVAL,\n});\n\nconst chainId = await publicClient.getChainId();\nconst database = drizzle(postgres(env.DATABASE_URL, { prepare: false }));\n\nconst { storageAdapter, tables } = await createStorageAdapter({ database, publicClient });\n\nlet startBlock = env.START_BLOCK;\n\n// Resume from latest block stored in DB. This will throw if the DB doesn't exist yet, so we wrap in a try/catch and ignore the error.\n// TODO: query if the DB exists instead of try/catch\ntry {\n const chainState = await database\n .select()\n .from(tables.configTable)\n .where(eq(tables.configTable.chainId, chainId))\n .limit(1)\n .execute()\n // Get the first record in a way that returns a possible `undefined`\n // TODO: move this to `.findFirst` after upgrading drizzle or `rows[0]` after enabling `noUncheckedIndexedAccess: true`\n .then((rows) => rows.find(() => true));\n\n if (chainState?.blockNumber != null) {\n startBlock = chainState.blockNumber + 1n;\n console.log(\"resuming from block number\", startBlock);\n }\n} catch (error) {\n // ignore errors for now\n}\n\nconst { latestBlockNumber$, storedBlockLogs$ } = await createStoreSync({\n storageAdapter,\n publicClient,\n startBlock,\n maxBlockRange: env.MAX_BLOCK_RANGE,\n address: env.STORE_ADDRESS,\n});\n\nstoredBlockLogs$.subscribe();\n\nlet isCaughtUp = false;\ncombineLatest([latestBlockNumber$, storedBlockLogs$])\n .pipe(\n filter(\n ([latestBlockNumber, { blockNumber: lastBlockNumberProcessed }]) => latestBlockNumber === lastBlockNumberProcessed\n ),\n first()\n )\n .subscribe(() => {\n isCaughtUp = true;\n console.log(\"all caught up\");\n });\n\nif (env.HEALTHCHECK_HOST != null || env.HEALTHCHECK_PORT != null) {\n const { default: Koa } = await import(\"koa\");\n const { default: cors } = await import(\"@koa/cors\");\n const { default: Router } = await import(\"@koa/router\");\n\n const server = new Koa();\n server.use(cors());\n\n const router = new Router();\n\n router.get(\"/\", (ctx) => {\n ctx.body = \"emit HelloWorld();\";\n });\n\n // k8s healthchecks\n router.get(\"/healthz\", (ctx) => {\n ctx.status = 200;\n });\n router.get(\"/readyz\", (ctx) => {\n if (isCaughtUp) {\n ctx.status = 200;\n ctx.body = \"ready\";\n } else {\n ctx.status = 424;\n ctx.body = \"backfilling\";\n }\n });\n\n server.use(router.routes());\n server.use(router.allowedMethods());\n\n server.listen({ host: env.HEALTHCHECK_HOST, port: env.HEALTHCHECK_PORT });\n console.log(\n `postgres indexer healthcheck server listening on http://${env.HEALTHCHECK_HOST}:${env.HEALTHCHECK_PORT}`\n );\n}\n"],"mappings":"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","names":["z","eq","createPublicClient","fallback","webSocket","http","isDefined","combineLatest","filter","first","drizzle","postgres","createStorageAdapter","createStoreSync","env","parseEnv","z","indexerEnvSchema","transports","webSocket","http","isDefined","publicClient","createPublicClient","fallback","chainId","database","drizzle","postgres","storageAdapter","tables","createStorageAdapter","startBlock","chainState","eq","rows","latestBlockNumber$","storedBlockLogs$","createStoreSync","isCaughtUp","combineLatest","filter","latestBlockNumber","lastBlockNumberProcessed","first","Koa","cors","Router","server","router","ctx"]}
@@ -1,3 +1,31 @@
1
1
  #!/usr/bin/env node
2
- import{a as A}from"../chunk-LOFAZSVJ.js";import{a as g,c as h}from"../chunk-2E5MDUA2.js";import"dotenv/config";import{z as l}from"zod";import _ from"fastify";import{fastifyTRPCPlugin as j}from"@trpc/server/adapters/fastify";import{createAppRouter as q}from"@latticexyz/store-sync/trpc-indexer";import{eq as T}from"drizzle-orm";import{buildTable as x,buildInternalTables as D,getTables as E}from"@latticexyz/store-sync/postgres";import{getAddress as k}from"viem";import{decodeDynamicField as I}from"@latticexyz/protocol-parser";async function w(r){return{async findAll({chainId:p,address:c,filters:m=[]}){let d=Array.from(new Set(m.map(e=>e.tableId))),R=(await E(r)).filter(e=>c==null||k(c)===k(e.address)).filter(e=>!d.length||d.includes(e.tableId)),O=await Promise.all(R.map(async e=>{let y=x(e),b=await r.select().from(y).where(T(y.__isDeleted,!1)).execute(),v=m.length?b.filter(i=>{let t=I("bytes32[]",i.__key);return m.some(o=>o.tableId===e.tableId&&(o.key0==null||o.key0===t[0])&&(o.key1==null||o.key1===t[1]))}):b;return{...e,records:v.map(i=>({key:Object.fromEntries(Object.entries(e.keySchema).map(([t])=>[t,i[t]])),value:Object.fromEntries(Object.entries(e.valueSchema).map(([t])=>[t,i[t]]))}))}})),u=D(),P=await r.select().from(u.chain).where(T(u.chain.chainId,p)).execute(),{lastUpdatedBlockNumber:S}=P[0]??{},f={blockNumber:S??null,tables:O};return A("findAll",p,c,f),f}}}import{drizzle as z}from"drizzle-orm/postgres-js";import Q from"postgres";var a=h(l.intersection(g,l.object({DATABASE_URL:l.string()}))),B=z(Q(a.DATABASE_URL)),n=_({maxParamLength:5e3});await n.register(import("@fastify/cors"));n.get("/healthz",(r,s)=>s.code(200).send());n.get("/readyz",(r,s)=>s.code(200).send());n.register(j,{prefix:"/trpc",trpcOptions:{router:q(),createContext:async()=>({queryAdapter:await w(B)})}});await n.listen({host:a.HOST,port:a.PORT});console.log(`postgres indexer frontend listening on http://${a.HOST}:${a.PORT}`);
2
+ import{a as S,b as T,c as $,d as x}from"../chunk-C47XPAJP.js";import{a as N,c as L}from"../chunk-2E5MDUA2.js";import"dotenv/config";import{z as A}from"zod";import re from"koa";import oe from"@koa/cors";import te from"@koa/router";import{createKoaMiddleware as ne}from"trpc-koa-adapter";import{createAppRouter as ae}from"@latticexyz/store-sync/trpc-indexer";import{drizzle as se}from"drizzle-orm/postgres-js";import de from"postgres";import{getAddress as w}from"viem";import{isTableRegistrationLog as H,logToTable as W,storeTables as J}from"@latticexyz/store-sync";import{decodeKey as K,decodeValueArgs as U}from"@latticexyz/protocol-parser";import{tables as i}from"@latticexyz/store-sync/postgres";import{and as v,asc as Q,eq as m,or as z}from"drizzle-orm";import{bigIntMax as C}from"@latticexyz/common/utils";import{decodeDynamicField as M}from"@latticexyz/protocol-parser";function h(e){return{address:e.address,eventName:"Store_SetRecord",args:{tableId:e.tableId,keyTuple:M("bytes32[]",e.keyBytes),staticData:e.staticData??"0x",encodedLengths:e.encodedLengths??"0x",dynamicData:e.dynamicData??"0x"}}}import{createBenchmark as F}from"@latticexyz/common";async function k(e,{chainId:t,address:r,filters:a=[]}){let o=F("drizzleGetLogs"),n=a.length?a.map(s=>v(r!=null?m(i.recordsTable.address,r):void 0,m(i.recordsTable.tableId,s.tableId),s.key0!=null?m(i.recordsTable.key0,s.key0):void 0,s.key1!=null?m(i.recordsTable.key1,s.key1):void 0)):r!=null?[m(i.recordsTable.address,r)]:[];o("parse config");let u=(await e.select().from(i.configTable).where(m(i.configTable.chainId,t)).limit(1).execute().then(s=>s.find(()=>!0)))?.blockNumber??0n;o("query chainState");let y=await e.select().from(i.recordsTable).where(z(...n)).orderBy(Q(i.recordsTable.blockNumber));o("query records");let d=y.reduce((s,b)=>C(s,b.blockNumber??0n),u);o("find block number");let R=y.filter(s=>!s.isDeleted).map(h);return o("map records to logs"),{blockNumber:d,logs:R}}import{groupBy as V}from"@latticexyz/common/utils";async function I(e){return{async getLogs(r){return k(e,r)},async findAll(r){let a=r.filters??[],{blockNumber:o,logs:n}=await k(e,{...r,filters:a.length>0?[...a,{tableId:J.Tables.tableId}]:[]}),l=n.filter(H).map(W),u=V(n,d=>`${w(d.address)}:${d.args.tableId}`),y=l.map(d=>{let s=(u.get(`${w(d.address)}:${d.tableId}`)??[]).map(b=>({key:K(d.keySchema,b.args.keyTuple),value:U(d.valueSchema,b.args)}));return{...d,records:s}});return S("findAll: decoded %d logs across %d tables",n.length,l.length),{blockNumber:o,tables:y}}}}import q from"@koa/router";import G from"koa-compose";import{input as X}from"@latticexyz/store-sync/indexer-client";import{storeTables as Z}from"@latticexyz/store-sync";import{isNotNull as D}from"@latticexyz/common/utils";import{hexToBytes as p}from"viem";import{transformSchemaName as Y}from"@latticexyz/store-sync/postgres";var _=Y("mud");function O(e,t){return e`(${t.reduce((r,a)=>e`${r} AND ${a}`)})`}function j(e,t){return e`(${t.reduce((r,a)=>e`${r} OR ${a}`)})`}function B(e,t){let r=t.filters.length?t.filters.map(o=>O(e,[t.address!=null?e`address = ${p(t.address)}`:null,e`table_id = ${p(o.tableId)}`,o.key0!=null?e`key0 = ${p(o.key0)}`:null,o.key1!=null?e`key1 = ${p(o.key1)}`:null].filter(D))):t.address!=null?[e`address = ${p(t.address)}`]:[],a=e`WHERE ${O(e,[e`is_deleted != true`,r.length?j(e,r):null].filter(D))}`;return e`
3
+ WITH
4
+ config AS (
5
+ SELECT
6
+ version AS "indexerVersion",
7
+ chain_id AS "chainId",
8
+ block_number AS "chainBlockNumber"
9
+ FROM ${e(`${_}.config`)}
10
+ LIMIT 1
11
+ ),
12
+ records AS (
13
+ SELECT
14
+ '0x' || encode(address, 'hex') AS address,
15
+ '0x' || encode(table_id, 'hex') AS "tableId",
16
+ '0x' || encode(key_bytes, 'hex') AS "keyBytes",
17
+ '0x' || encode(static_data, 'hex') AS "staticData",
18
+ '0x' || encode(encoded_lengths, 'hex') AS "encodedLengths",
19
+ '0x' || encode(dynamic_data, 'hex') AS "dynamicData",
20
+ block_number AS "recordBlockNumber",
21
+ log_index AS "logIndex"
22
+ FROM ${e(`${_}.records`)}
23
+ ${a}
24
+ ORDER BY block_number, log_index ASC
25
+ )
26
+ SELECT
27
+ (SELECT COUNT(*) FROM records) AS "totalRows",
28
+ *
29
+ FROM config, records
30
+ `}import{createBenchmark as ee}from"@latticexyz/common";function E(e){let t=new q;return t.get("/api/logs",$(),async r=>{let a=ee("postgres:logs"),o;try{o=X.parse(typeof r.query.input=="string"?JSON.parse(r.query.input):{})}catch(n){r.status=400,r.body=JSON.stringify(n),S(n);return}try{o.filters=o.filters.length>0?[...o.filters,{tableId:Z.Tables.tableId}]:[];let n=await B(e,o??{}).execute();a("query records");let l=n.map(h);if(a("map records to logs"),n.length===0){r.status=404,r.body="no logs found",T(`no logs found for chainId ${o.chainId}, address ${o.address}, filters ${JSON.stringify(o.filters)}`);return}let u=n[0].chainBlockNumber;r.body=JSON.stringify({blockNumber:u,logs:l}),r.status=200}catch(n){r.status=500,r.body=JSON.stringify(n),T(n)}}),G([t.routes(),t.allowedMethods()])}var f=L(A.intersection(N,A.object({DATABASE_URL:A.string()}))),P=de(f.DATABASE_URL,{prepare:!1}),c=new re;process.env.SENTRY_DSN&&x(c);c.use(oe());c.use(E(P));var g=new te;g.get("/",e=>{e.body="emit HelloWorld();"});g.get("/healthz",e=>{e.status=200});g.get("/readyz",e=>{e.status=200});c.use(g.routes());c.use(g.allowedMethods());c.use(ne({prefix:"/trpc",router:ae(),createContext:async()=>({queryAdapter:await I(se(P))})}));c.listen({host:f.HOST,port:f.PORT});console.log(`postgres indexer frontend listening on http://${f.HOST}:${f.PORT}`);
3
31
  //# sourceMappingURL=postgres-frontend.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../bin/postgres-frontend.ts","../../src/postgres/createQueryAdapter.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { z } from \"zod\";\nimport fastify from \"fastify\";\nimport { fastifyTRPCPlugin } from \"@trpc/server/adapters/fastify\";\nimport { AppRouter, createAppRouter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { createQueryAdapter } from \"../src/postgres/createQueryAdapter\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { frontendEnvSchema, parseEnv } from \"./parseEnv\";\n\nconst env = parseEnv(\n z.intersection(\n frontendEnvSchema,\n z.object({\n DATABASE_URL: z.string(),\n })\n )\n);\n\nconst database = drizzle(postgres(env.DATABASE_URL));\n\n// @see https://fastify.dev/docs/latest/\nconst server = fastify({\n maxParamLength: 5000,\n});\n\nawait server.register(import(\"@fastify/cors\"));\n\n// k8s healthchecks\nserver.get(\"/healthz\", (req, res) => res.code(200).send());\nserver.get(\"/readyz\", (req, res) => res.code(200).send());\n\n// @see https://trpc.io/docs/server/adapters/fastify\nserver.register(fastifyTRPCPlugin<AppRouter>, {\n prefix: \"/trpc\",\n trpcOptions: {\n router: createAppRouter(),\n createContext: async () => ({\n queryAdapter: await createQueryAdapter(database),\n }),\n },\n});\n\nawait server.listen({ host: env.HOST, port: env.PORT });\nconsole.log(`postgres indexer frontend listening on http://${env.HOST}:${env.PORT}`);\n","import { eq } from \"drizzle-orm\";\nimport { PgDatabase } from \"drizzle-orm/pg-core\";\nimport { buildTable, buildInternalTables, getTables } from \"@latticexyz/store-sync/postgres\";\nimport { QueryAdapter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { debug } from \"../debug\";\nimport { getAddress } from \"viem\";\nimport { decodeDynamicField } from \"@latticexyz/protocol-parser\";\n\n/**\n * Creates a query adapter for the tRPC server/client to query data from Postgres.\n *\n * @param {PgDatabase<any>} database Postgres database object from Drizzle\n * @returns {Promise<QueryAdapter>} A set of methods used by tRPC endpoints.\n */\nexport async function createQueryAdapter(database: PgDatabase<any>): Promise<QueryAdapter> {\n const adapter: QueryAdapter = {\n async findAll({ chainId, address, filters = [] }) {\n // If _any_ filter has a table ID, this will filter down all data to just those tables. Which mean we can't yet mix table filters with key-only filters.\n // TODO: improve this so we can express this in the query (need to be able to query data across tables more easily)\n const tableIds = Array.from(new Set(filters.map((filter) => filter.tableId)));\n const tables = (await getTables(database))\n .filter((table) => address == null || getAddress(address) === getAddress(table.address))\n .filter((table) => !tableIds.length || tableIds.includes(table.tableId));\n\n const tablesWithRecords = await Promise.all(\n tables.map(async (table) => {\n const sqliteTable = buildTable(table);\n const records = await database.select().from(sqliteTable).where(eq(sqliteTable.__isDeleted, false)).execute();\n const filteredRecords = !filters.length\n ? records\n : records.filter((record) => {\n const keyTuple = decodeDynamicField(\"bytes32[]\", record.__key);\n return filters.some(\n (filter) =>\n filter.tableId === table.tableId &&\n (filter.key0 == null || filter.key0 === keyTuple[0]) &&\n (filter.key1 == null || filter.key1 === keyTuple[1])\n );\n });\n return {\n ...table,\n records: filteredRecords.map((record) => ({\n key: Object.fromEntries(Object.entries(table.keySchema).map(([name]) => [name, record[name]])),\n value: Object.fromEntries(Object.entries(table.valueSchema).map(([name]) => [name, record[name]])),\n })),\n };\n })\n );\n\n const internalTables = buildInternalTables();\n const metadata = await database\n .select()\n .from(internalTables.chain)\n .where(eq(internalTables.chain.chainId, chainId))\n .execute();\n const { lastUpdatedBlockNumber } = metadata[0] ?? {};\n\n const result = {\n blockNumber: lastUpdatedBlockNumber ?? null,\n tables: tablesWithRecords,\n };\n\n debug(\"findAll\", chainId, address, result);\n\n return result;\n },\n };\n return adapter;\n}\n"],"mappings":"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","names":["z","fastify","fastifyTRPCPlugin","createAppRouter","eq","buildTable","buildInternalTables","getTables","getAddress","decodeDynamicField","createQueryAdapter","database","chainId","address","filters","tableIds","filter","tables","getTables","table","tablesWithRecords","sqliteTable","buildTable","records","eq","filteredRecords","record","keyTuple","name","internalTables","buildInternalTables","metadata","lastUpdatedBlockNumber","result","debug","drizzle","postgres","env","parseEnv","z","frontendEnvSchema","database","drizzle","postgres","server","fastify","req","res","fastifyTRPCPlugin","createAppRouter","createQueryAdapter"]}
1
+ {"version":3,"sources":["../../bin/postgres-frontend.ts","../../src/postgres/deprecated/createQueryAdapter.ts","../../src/postgres/deprecated/getLogs.ts","../../src/postgres/recordToLog.ts","../../src/postgres/apiRoutes.ts","../../src/postgres/queryLogs.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { z } from \"zod\";\nimport Koa from \"koa\";\nimport cors from \"@koa/cors\";\nimport Router from \"@koa/router\";\nimport { createKoaMiddleware } from \"trpc-koa-adapter\";\nimport { createAppRouter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { frontendEnvSchema, parseEnv } from \"./parseEnv\";\nimport { createQueryAdapter } from \"../src/postgres/deprecated/createQueryAdapter\";\nimport { apiRoutes } from \"../src/postgres/apiRoutes\";\nimport { registerSentryMiddlewares } from \"../src/sentry\";\n\nconst env = parseEnv(\n z.intersection(\n frontendEnvSchema,\n z.object({\n DATABASE_URL: z.string(),\n })\n )\n);\n\nconst database = postgres(env.DATABASE_URL, { prepare: false });\n\nconst server = new Koa();\n\nif (process.env.SENTRY_DSN) {\n registerSentryMiddlewares(server);\n}\n\nserver.use(cors());\nserver.use(apiRoutes(database));\n\nconst router = new Router();\n\nrouter.get(\"/\", (ctx) => {\n ctx.body = \"emit HelloWorld();\";\n});\n\n// k8s healthchecks\nrouter.get(\"/healthz\", (ctx) => {\n ctx.status = 200;\n});\nrouter.get(\"/readyz\", (ctx) => {\n ctx.status = 200;\n});\n\nserver.use(router.routes());\nserver.use(router.allowedMethods());\n\nserver.use(\n createKoaMiddleware({\n prefix: \"/trpc\",\n router: createAppRouter(),\n createContext: async () => ({\n queryAdapter: await createQueryAdapter(drizzle(database)),\n }),\n })\n);\n\nserver.listen({ host: env.HOST, port: env.PORT });\nconsole.log(`postgres indexer frontend listening on http://${env.HOST}:${env.PORT}`);\n","import { getAddress } from \"viem\";\nimport { PgDatabase } from \"drizzle-orm/pg-core\";\nimport { TableWithRecords, isTableRegistrationLog, logToTable, storeTables } from \"@latticexyz/store-sync\";\nimport { decodeKey, decodeValueArgs } from \"@latticexyz/protocol-parser\";\nimport { QueryAdapter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { debug } from \"../../debug\";\nimport { getLogs } from \"./getLogs\";\nimport { groupBy } from \"@latticexyz/common/utils\";\n\n/**\n * Creates a query adapter for the tRPC server/client to query data from Postgres.\n *\n * @param {PgDatabase<any>} database Postgres database object from Drizzle\n * @returns {Promise<QueryAdapter>} A set of methods used by tRPC endpoints.\n * @deprecated\n */\nexport async function createQueryAdapter(database: PgDatabase<any>): Promise<QueryAdapter> {\n const adapter: QueryAdapter = {\n async getLogs(opts) {\n return getLogs(database, opts);\n },\n async findAll(opts) {\n const filters = opts.filters ?? [];\n const { blockNumber, logs } = await getLogs(database, {\n ...opts,\n // make sure we're always retrieving `store.Tables` table, so we can decode table values\n filters: filters.length > 0 ? [...filters, { tableId: storeTables.Tables.tableId }] : [],\n });\n\n const tables = logs.filter(isTableRegistrationLog).map(logToTable);\n\n const logsByTable = groupBy(logs, (log) => `${getAddress(log.address)}:${log.args.tableId}`);\n\n const tablesWithRecords: TableWithRecords[] = tables.map((table) => {\n const tableLogs = logsByTable.get(`${getAddress(table.address)}:${table.tableId}`) ?? [];\n const records = tableLogs.map((log) => ({\n key: decodeKey(table.keySchema, log.args.keyTuple),\n value: decodeValueArgs(table.valueSchema, log.args),\n }));\n\n return {\n ...table,\n records,\n };\n });\n\n debug(\"findAll: decoded %d logs across %d tables\", logs.length, tables.length);\n\n return {\n blockNumber,\n tables: tablesWithRecords,\n };\n },\n };\n return adapter;\n}\n","import { PgDatabase } from \"drizzle-orm/pg-core\";\nimport { Hex } from \"viem\";\nimport { StorageAdapterLog, SyncFilter } from \"@latticexyz/store-sync\";\nimport { tables } from \"@latticexyz/store-sync/postgres\";\nimport { and, asc, eq, or } from \"drizzle-orm\";\nimport { bigIntMax } from \"@latticexyz/common/utils\";\nimport { recordToLog } from \"../recordToLog\";\nimport { createBenchmark } from \"@latticexyz/common\";\n\n/**\n * @deprecated\n */\nexport async function getLogs(\n database: PgDatabase<any>,\n {\n chainId,\n address,\n filters = [],\n }: {\n readonly chainId: number;\n readonly address?: Hex;\n readonly filters?: readonly SyncFilter[];\n }\n): Promise<{ blockNumber: bigint; logs: (StorageAdapterLog & { eventName: \"Store_SetRecord\" })[] }> {\n const benchmark = createBenchmark(\"drizzleGetLogs\");\n\n const conditions = filters.length\n ? filters.map((filter) =>\n and(\n address != null ? eq(tables.recordsTable.address, address) : undefined,\n eq(tables.recordsTable.tableId, filter.tableId),\n filter.key0 != null ? eq(tables.recordsTable.key0, filter.key0) : undefined,\n filter.key1 != null ? eq(tables.recordsTable.key1, filter.key1) : undefined\n )\n )\n : address != null\n ? [eq(tables.recordsTable.address, address)]\n : [];\n benchmark(\"parse config\");\n\n // Query for the block number that the indexer (i.e. chain) is at, in case the\n // indexer is further along in the chain than a given store/table's last updated\n // block number. We'll then take the highest block number between the indexer's\n // chain state and all the records in the query (in case the records updated\n // between these queries). Using just the highest block number from the queries\n // could potentially signal to the client an older-than-necessary block number,\n // for stores/tables that haven't seen recent activity.\n // TODO: move the block number query into the records query for atomicity so we don't have to merge them here\n const chainState = await database\n .select()\n .from(tables.configTable)\n .where(eq(tables.configTable.chainId, chainId))\n .limit(1)\n .execute()\n // Get the first record in a way that returns a possible `undefined`\n // TODO: move this to `.findFirst` after upgrading drizzle or `rows[0]` after enabling `noUncheckedIndexedAccess: true`\n .then((rows) => rows.find(() => true));\n const indexerBlockNumber = chainState?.blockNumber ?? 0n;\n benchmark(\"query chainState\");\n\n const records = await database\n .select()\n .from(tables.recordsTable)\n .where(or(...conditions))\n .orderBy(\n asc(tables.recordsTable.blockNumber)\n // TODO: add logIndex (https://github.com/latticexyz/mud/issues/1979)\n );\n benchmark(\"query records\");\n\n const blockNumber = records.reduce((max, record) => bigIntMax(max, record.blockNumber ?? 0n), indexerBlockNumber);\n benchmark(\"find block number\");\n\n const logs = records\n // TODO: add this to the query, assuming we can optimize with an index\n .filter((record) => !record.isDeleted)\n .map(recordToLog);\n benchmark(\"map records to logs\");\n\n return { blockNumber, logs };\n}\n","import { StorageAdapterLog } from \"@latticexyz/store-sync\";\nimport { decodeDynamicField } from \"@latticexyz/protocol-parser\";\nimport { RecordData } from \"./common\";\n\nexport function recordToLog(\n record: Omit<RecordData, \"recordBlockNumber\">\n): StorageAdapterLog & { eventName: \"Store_SetRecord\" } {\n return {\n address: record.address,\n eventName: \"Store_SetRecord\",\n args: {\n tableId: record.tableId,\n keyTuple: decodeDynamicField(\"bytes32[]\", record.keyBytes),\n staticData: record.staticData ?? \"0x\",\n encodedLengths: record.encodedLengths ?? \"0x\",\n dynamicData: record.dynamicData ?? \"0x\",\n },\n } as const;\n}\n","import { Sql } from \"postgres\";\nimport { Middleware } from \"koa\";\nimport Router from \"@koa/router\";\nimport compose from \"koa-compose\";\nimport { input } from \"@latticexyz/store-sync/indexer-client\";\nimport { storeTables } from \"@latticexyz/store-sync\";\nimport { queryLogs } from \"./queryLogs\";\nimport { recordToLog } from \"./recordToLog\";\nimport { debug, error } from \"../debug\";\nimport { createBenchmark } from \"@latticexyz/common\";\nimport { compress } from \"../compress\";\n\nexport function apiRoutes(database: Sql): Middleware {\n const router = new Router();\n\n router.get(\"/api/logs\", compress(), async (ctx) => {\n const benchmark = createBenchmark(\"postgres:logs\");\n let options: ReturnType<typeof input.parse>;\n\n try {\n options = input.parse(typeof ctx.query.input === \"string\" ? JSON.parse(ctx.query.input) : {});\n } catch (e) {\n ctx.status = 400;\n ctx.body = JSON.stringify(e);\n debug(e);\n return;\n }\n\n try {\n options.filters = options.filters.length > 0 ? [...options.filters, { tableId: storeTables.Tables.tableId }] : [];\n const records = await queryLogs(database, options ?? {}).execute();\n benchmark(\"query records\");\n const logs = records.map(recordToLog);\n benchmark(\"map records to logs\");\n\n if (records.length === 0) {\n ctx.status = 404;\n ctx.body = \"no logs found\";\n error(\n `no logs found for chainId ${options.chainId}, address ${options.address}, filters ${JSON.stringify(\n options.filters\n )}`\n );\n return;\n }\n\n const blockNumber = records[0].chainBlockNumber;\n ctx.body = JSON.stringify({ blockNumber, logs });\n ctx.status = 200;\n } catch (e) {\n ctx.status = 500;\n ctx.body = JSON.stringify(e);\n error(e);\n }\n });\n\n return compose([router.routes(), router.allowedMethods()]) as Middleware;\n}\n","import { isNotNull } from \"@latticexyz/common/utils\";\nimport { PendingQuery, Row, Sql } from \"postgres\";\nimport { hexToBytes } from \"viem\";\nimport { z } from \"zod\";\nimport { input } from \"@latticexyz/store-sync/indexer-client\";\nimport { transformSchemaName } from \"@latticexyz/store-sync/postgres\";\nimport { Record } from \"./common\";\n\nconst schemaName = transformSchemaName(\"mud\");\n\nfunction and(sql: Sql, conditions: PendingQuery<Row[]>[]): PendingQuery<Row[]> {\n return sql`(${conditions.reduce((query, condition) => sql`${query} AND ${condition}`)})`;\n}\n\nfunction or(sql: Sql, conditions: PendingQuery<Row[]>[]): PendingQuery<Row[]> {\n return sql`(${conditions.reduce((query, condition) => sql`${query} OR ${condition}`)})`;\n}\n\nexport function queryLogs(sql: Sql, opts: z.infer<typeof input>): PendingQuery<Record[]> {\n const conditions = opts.filters.length\n ? opts.filters.map((filter) =>\n and(\n sql,\n [\n opts.address != null ? sql`address = ${hexToBytes(opts.address)}` : null,\n sql`table_id = ${hexToBytes(filter.tableId)}`,\n filter.key0 != null ? sql`key0 = ${hexToBytes(filter.key0)}` : null,\n filter.key1 != null ? sql`key1 = ${hexToBytes(filter.key1)}` : null,\n ].filter(isNotNull)\n )\n )\n : opts.address != null\n ? [sql`address = ${hexToBytes(opts.address)}`]\n : [];\n\n const where = sql`WHERE ${and(\n sql,\n [sql`is_deleted != true`, conditions.length ? or(sql, conditions) : null].filter(isNotNull)\n )}`;\n\n // TODO: implement bytea <> hex columns via custom types: https://github.com/porsager/postgres#custom-types\n // TODO: sort by logIndex (https://github.com/latticexyz/mud/issues/1979)\n return sql<Record[]>`\n WITH\n config AS (\n SELECT\n version AS \"indexerVersion\",\n chain_id AS \"chainId\",\n block_number AS \"chainBlockNumber\"\n FROM ${sql(`${schemaName}.config`)}\n LIMIT 1\n ),\n records AS (\n SELECT\n '0x' || encode(address, 'hex') AS address,\n '0x' || encode(table_id, 'hex') AS \"tableId\",\n '0x' || encode(key_bytes, 'hex') AS \"keyBytes\",\n '0x' || encode(static_data, 'hex') AS \"staticData\",\n '0x' || encode(encoded_lengths, 'hex') AS \"encodedLengths\",\n '0x' || encode(dynamic_data, 'hex') AS \"dynamicData\",\n block_number AS \"recordBlockNumber\",\n log_index AS \"logIndex\"\n FROM ${sql(`${schemaName}.records`)}\n ${where}\n ORDER BY block_number, log_index ASC\n )\n SELECT\n (SELECT COUNT(*) FROM records) AS \"totalRows\",\n *\n FROM config, records\n `;\n}\n"],"mappings":"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","names":["z","Koa","cors","Router","createKoaMiddleware","createAppRouter","drizzle","postgres","getAddress","isTableRegistrationLog","logToTable","storeTables","decodeKey","decodeValueArgs","tables","and","asc","eq","or","bigIntMax","decodeDynamicField","recordToLog","record","createBenchmark","getLogs","database","chainId","address","filters","benchmark","conditions","filter","and","eq","tables","indexerBlockNumber","rows","records","or","asc","blockNumber","max","record","bigIntMax","logs","recordToLog","groupBy","createQueryAdapter","database","opts","getLogs","filters","blockNumber","logs","storeTables","tables","isTableRegistrationLog","logToTable","logsByTable","log","getAddress","tablesWithRecords","table","records","decodeKey","decodeValueArgs","debug","Router","compose","input","storeTables","isNotNull","hexToBytes","transformSchemaName","schemaName","and","sql","conditions","query","condition","or","queryLogs","opts","filter","where","createBenchmark","apiRoutes","database","router","Router","compress","ctx","benchmark","createBenchmark","options","input","e","debug","storeTables","records","queryLogs","logs","recordToLog","error","blockNumber","compose","env","parseEnv","z","frontendEnvSchema","database","postgres","server","Koa","registerSentryMiddlewares","cors","apiRoutes","router","Router","ctx","createKoaMiddleware","createAppRouter","createQueryAdapter","drizzle"]}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{b as c,c as l}from"../chunk-2E5MDUA2.js";import"dotenv/config";import{z as n}from"zod";import{eq as b}from"drizzle-orm";import{createPublicClient as h,fallback as C,webSocket as T,http as g}from"viem";import{isDefined as H}from"@latticexyz/common/utils";import{combineLatest as _,filter as E,first as S}from"rxjs";import{drizzle as L}from"drizzle-orm/postgres-js";import R from"postgres";import{cleanDatabase as A,postgresStorage as k,schemaVersion as m}from"@latticexyz/store-sync/postgres";import{createStoreSync as B}from"@latticexyz/store-sync";var e=l(n.intersection(c,n.object({DATABASE_URL:n.string(),HEALTHCHECK_HOST:n.string().optional(),HEALTHCHECK_PORT:n.coerce.number().optional()}))),P=[e.RPC_WS_URL?T(e.RPC_WS_URL):void 0,e.RPC_HTTP_URL?g(e.RPC_HTTP_URL):void 0].filter(H),s=h({transport:C(P),pollingInterval:e.POLLING_INTERVAL}),O=await s.getChainId(),a=L(R(e.DATABASE_URL)),{storageAdapter:U,internalTables:d}=await k({database:a,publicClient:s}),p=e.START_BLOCK;try{let t=(await a.select().from(d.chain).where(b(d.chain.chainId,O)).execute())[0];t!=null&&(t.schemaVersion!=m?(console.log("schema version changed from",t.schemaVersion,"to",m,"cleaning database"),await A(a)):t.lastUpdatedBlockNumber!=null&&(console.log("resuming from block number",t.lastUpdatedBlockNumber+1n),p=t.lastUpdatedBlockNumber+1n))}catch{}var{latestBlockNumber$:v,storedBlockLogs$:f}=await B({storageAdapter:U,publicClient:s,startBlock:p,maxBlockRange:e.MAX_BLOCK_RANGE,address:e.STORE_ADDRESS});f.subscribe();var u=!1;_([v,f]).pipe(E(([o,{blockNumber:t}])=>o===t),S()).subscribe(()=>{u=!0,console.log("all caught up")});if(e.HEALTHCHECK_HOST!=null||e.HEALTHCHECK_PORT!=null){let{default:o}=await import("fastify"),t=o();t.get("/healthz",(i,r)=>r.code(200).send()),t.get("/readyz",(i,r)=>u?r.code(200).send("ready"):r.code(424).send("backfilling")),t.listen({host:e.HEALTHCHECK_HOST,port:e.HEALTHCHECK_PORT},(i,r)=>{console.log(`postgres indexer healthcheck server listening on ${r}`)})}
2
+ import{b as m,c as d}from"../chunk-2E5MDUA2.js";import"dotenv/config";import{z as s}from"zod";import{eq as T}from"drizzle-orm";import{createPublicClient as C,fallback as g,webSocket as E,http as _}from"viem";import{isDefined as h}from"@latticexyz/common/utils";import{combineLatest as A,filter as L,first as R}from"rxjs";import{drizzle as S}from"drizzle-orm/postgres-js";import w from"postgres";import{cleanDatabase as k,createStorageAdapter as O,shouldCleanDatabase as P}from"@latticexyz/store-sync/postgres";import{createStoreSync as K}from"@latticexyz/store-sync";var e=d(s.intersection(m,s.object({DATABASE_URL:s.string(),HEALTHCHECK_HOST:s.string().optional(),HEALTHCHECK_PORT:s.coerce.number().optional()}))),B=[e.RPC_WS_URL?E(e.RPC_WS_URL):void 0,e.RPC_HTTP_URL?_(e.RPC_HTTP_URL):void 0].filter(h),c=C({transport:g(B),pollingInterval:e.POLLING_INTERVAL}),p=await c.getChainId(),i=S(w(e.DATABASE_URL,{prepare:!1}));await P(i,p)&&(console.log("outdated database detected, clearing data to start fresh"),await k(i));var{storageAdapter:v,tables:u}=await O({database:i,publicClient:c}),l=e.START_BLOCK;try{let o=await i.select().from(u.configTable).where(T(u.configTable.chainId,p)).limit(1).execute().then(r=>r.find(()=>!0));o?.blockNumber!=null&&(l=o.blockNumber+1n,console.log("resuming from block number",l))}catch{}var{latestBlockNumber$:y,storedBlockLogs$:f}=await K({storageAdapter:v,publicClient:c,startBlock:l,maxBlockRange:e.MAX_BLOCK_RANGE,address:e.STORE_ADDRESS});f.subscribe();var b=!1;A([y,f]).pipe(L(([o,{blockNumber:r}])=>o===r),R()).subscribe(()=>{b=!0,console.log("all caught up")});if(e.HEALTHCHECK_HOST!=null||e.HEALTHCHECK_PORT!=null){let{default:o}=await import("koa"),{default:r}=await import("@koa/cors"),{default:H}=await import("@koa/router"),n=new o;n.use(r());let a=new H;a.get("/",t=>{t.body="emit HelloWorld();"}),a.get("/healthz",t=>{t.status=200}),a.get("/readyz",t=>{b?(t.status=200,t.body="ready"):(t.status=424,t.body="backfilling")}),n.use(a.routes()),n.use(a.allowedMethods()),n.listen({host:e.HEALTHCHECK_HOST,port:e.HEALTHCHECK_PORT}),console.log(`postgres indexer healthcheck server listening on http://${e.HEALTHCHECK_HOST}:${e.HEALTHCHECK_PORT}`)}
3
3
  //# sourceMappingURL=postgres-indexer.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../bin/postgres-indexer.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { z } from \"zod\";\nimport { eq } from \"drizzle-orm\";\nimport { createPublicClient, fallback, webSocket, http, Transport } from \"viem\";\nimport { isDefined } from \"@latticexyz/common/utils\";\nimport { combineLatest, filter, first } from \"rxjs\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { cleanDatabase, postgresStorage, schemaVersion } from \"@latticexyz/store-sync/postgres\";\nimport { createStoreSync } from \"@latticexyz/store-sync\";\nimport { indexerEnvSchema, parseEnv } from \"./parseEnv\";\n\nconst env = parseEnv(\n z.intersection(\n indexerEnvSchema,\n z.object({\n DATABASE_URL: z.string(),\n HEALTHCHECK_HOST: z.string().optional(),\n HEALTHCHECK_PORT: z.coerce.number().optional(),\n })\n )\n);\n\nconst transports: Transport[] = [\n // prefer WS when specified\n env.RPC_WS_URL ? webSocket(env.RPC_WS_URL) : undefined,\n // otherwise use or fallback to HTTP\n env.RPC_HTTP_URL ? http(env.RPC_HTTP_URL) : undefined,\n].filter(isDefined);\n\nconst publicClient = createPublicClient({\n transport: fallback(transports),\n pollingInterval: env.POLLING_INTERVAL,\n});\n\nconst chainId = await publicClient.getChainId();\nconst database = drizzle(postgres(env.DATABASE_URL));\n\nconst { storageAdapter, internalTables } = await postgresStorage({ database, publicClient });\n\nlet startBlock = env.START_BLOCK;\n\n// Resume from latest block stored in DB. This will throw if the DB doesn't exist yet, so we wrap in a try/catch and ignore the error.\ntry {\n const currentChainStates = await database\n .select()\n .from(internalTables.chain)\n .where(eq(internalTables.chain.chainId, chainId))\n .execute();\n // TODO: replace this type workaround with `noUncheckedIndexedAccess: true` when we can fix all the issues related (https://github.com/latticexyz/mud/issues/1212)\n const currentChainState: (typeof currentChainStates)[number] | undefined = currentChainStates[0];\n\n if (currentChainState != null) {\n if (currentChainState.schemaVersion != schemaVersion) {\n console.log(\n \"schema version changed from\",\n currentChainState.schemaVersion,\n \"to\",\n schemaVersion,\n \"cleaning database\"\n );\n await cleanDatabase(database);\n } else if (currentChainState.lastUpdatedBlockNumber != null) {\n console.log(\"resuming from block number\", currentChainState.lastUpdatedBlockNumber + 1n);\n startBlock = currentChainState.lastUpdatedBlockNumber + 1n;\n }\n }\n} catch (error) {\n // ignore errors, this is optional\n}\n\nconst { latestBlockNumber$, storedBlockLogs$ } = await createStoreSync({\n storageAdapter,\n publicClient,\n startBlock,\n maxBlockRange: env.MAX_BLOCK_RANGE,\n address: env.STORE_ADDRESS,\n});\n\nstoredBlockLogs$.subscribe();\n\nlet isCaughtUp = false;\ncombineLatest([latestBlockNumber$, storedBlockLogs$])\n .pipe(\n filter(\n ([latestBlockNumber, { blockNumber: lastBlockNumberProcessed }]) => latestBlockNumber === lastBlockNumberProcessed\n ),\n first()\n )\n .subscribe(() => {\n isCaughtUp = true;\n console.log(\"all caught up\");\n });\n\nif (env.HEALTHCHECK_HOST != null || env.HEALTHCHECK_PORT != null) {\n const { default: fastify } = await import(\"fastify\");\n\n const server = fastify();\n\n // k8s healthchecks\n server.get(\"/healthz\", (req, res) => res.code(200).send());\n server.get(\"/readyz\", (req, res) => (isCaughtUp ? res.code(200).send(\"ready\") : res.code(424).send(\"backfilling\")));\n\n server.listen({ host: env.HEALTHCHECK_HOST, port: env.HEALTHCHECK_PORT }, (error, address) => {\n console.log(`postgres indexer healthcheck server listening on ${address}`);\n });\n}\n"],"mappings":";gDACA,MAAO,gBACP,OAAS,KAAAA,MAAS,MAClB,OAAS,MAAAC,MAAU,cACnB,OAAS,sBAAAC,EAAoB,YAAAC,EAAU,aAAAC,EAAW,QAAAC,MAAuB,OACzE,OAAS,aAAAC,MAAiB,2BAC1B,OAAS,iBAAAC,EAAe,UAAAC,EAAQ,SAAAC,MAAa,OAC7C,OAAS,WAAAC,MAAe,0BACxB,OAAOC,MAAc,WACrB,OAAS,iBAAAC,EAAe,mBAAAC,EAAiB,iBAAAC,MAAqB,kCAC9D,OAAS,mBAAAC,MAAuB,yBAGhC,IAAMC,EAAMC,EACVC,EAAE,aACAC,EACAD,EAAE,OAAO,CACP,aAAcA,EAAE,OAAO,EACvB,iBAAkBA,EAAE,OAAO,EAAE,SAAS,EACtC,iBAAkBA,EAAE,OAAO,OAAO,EAAE,SAAS,CAC/C,CAAC,CACH,CACF,EAEME,EAA0B,CAE9BJ,EAAI,WAAaK,EAAUL,EAAI,UAAU,EAAI,OAE7CA,EAAI,aAAeM,EAAKN,EAAI,YAAY,EAAI,MAC9C,EAAE,OAAOO,CAAS,EAEZC,EAAeC,EAAmB,CACtC,UAAWC,EAASN,CAAU,EAC9B,gBAAiBJ,EAAI,gBACvB,CAAC,EAEKW,EAAU,MAAMH,EAAa,WAAW,EACxCI,EAAWC,EAAQC,EAASd,EAAI,YAAY,CAAC,EAE7C,CAAE,eAAAe,EAAgB,eAAAC,CAAe,EAAI,MAAMC,EAAgB,CAAE,SAAAL,EAAU,aAAAJ,CAAa,CAAC,EAEvFU,EAAalB,EAAI,YAGrB,GAAI,CAOF,IAAMmB,GANqB,MAAMP,EAC9B,OAAO,EACP,KAAKI,EAAe,KAAK,EACzB,MAAMI,EAAGJ,EAAe,MAAM,QAASL,CAAO,CAAC,EAC/C,QAAQ,GAEmF,CAAC,EAE3FQ,GAAqB,OACnBA,EAAkB,eAAiBE,GACrC,QAAQ,IACN,8BACAF,EAAkB,cAClB,KACAE,EACA,mBACF,EACA,MAAMC,EAAcV,CAAQ,GACnBO,EAAkB,wBAA0B,OACrD,QAAQ,IAAI,6BAA8BA,EAAkB,uBAAyB,EAAE,EACvFD,EAAaC,EAAkB,uBAAyB,IAG9D,MAAE,CAEF,CAEA,GAAM,CAAE,mBAAAI,EAAoB,iBAAAC,CAAiB,EAAI,MAAMC,EAAgB,CACrE,eAAAV,EACA,aAAAP,EACA,WAAAU,EACA,cAAelB,EAAI,gBACnB,QAASA,EAAI,aACf,CAAC,EAEDwB,EAAiB,UAAU,EAE3B,IAAIE,EAAa,GACjBC,EAAc,CAACJ,EAAoBC,CAAgB,CAAC,EACjD,KACCI,EACE,CAAC,CAACC,EAAmB,CAAE,YAAaC,CAAyB,CAAC,IAAMD,IAAsBC,CAC5F,EACAC,EAAM,CACR,EACC,UAAU,IAAM,CACfL,EAAa,GACb,QAAQ,IAAI,eAAe,CAC7B,CAAC,EAEH,GAAI1B,EAAI,kBAAoB,MAAQA,EAAI,kBAAoB,KAAM,CAChE,GAAM,CAAE,QAASgC,CAAQ,EAAI,KAAM,QAAO,SAAS,EAE7CC,EAASD,EAAQ,EAGvBC,EAAO,IAAI,WAAY,CAACC,EAAKC,IAAQA,EAAI,KAAK,GAAG,EAAE,KAAK,CAAC,EACzDF,EAAO,IAAI,UAAW,CAACC,EAAKC,IAAST,EAAaS,EAAI,KAAK,GAAG,EAAE,KAAK,OAAO,EAAIA,EAAI,KAAK,GAAG,EAAE,KAAK,aAAa,CAAE,EAElHF,EAAO,OAAO,CAAE,KAAMjC,EAAI,iBAAkB,KAAMA,EAAI,gBAAiB,EAAG,CAACoC,EAAOC,IAAY,CAC5F,QAAQ,IAAI,oDAAoDA,GAAS,CAC3E,CAAC","names":["z","eq","createPublicClient","fallback","webSocket","http","isDefined","combineLatest","filter","first","drizzle","postgres","cleanDatabase","postgresStorage","schemaVersion","createStoreSync","env","parseEnv","z","indexerEnvSchema","transports","webSocket","http","isDefined","publicClient","createPublicClient","fallback","chainId","database","drizzle","postgres","storageAdapter","internalTables","postgresStorage","startBlock","currentChainState","eq","schemaVersion","cleanDatabase","latestBlockNumber$","storedBlockLogs$","createStoreSync","isCaughtUp","combineLatest","filter","latestBlockNumber","lastBlockNumberProcessed","first","fastify","server","req","res","error","address"]}
1
+ {"version":3,"sources":["../../bin/postgres-indexer.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { z } from \"zod\";\nimport { eq } from \"drizzle-orm\";\nimport { createPublicClient, fallback, webSocket, http, Transport } from \"viem\";\nimport { isDefined } from \"@latticexyz/common/utils\";\nimport { combineLatest, filter, first } from \"rxjs\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { cleanDatabase, createStorageAdapter, shouldCleanDatabase } from \"@latticexyz/store-sync/postgres\";\nimport { createStoreSync } from \"@latticexyz/store-sync\";\nimport { indexerEnvSchema, parseEnv } from \"./parseEnv\";\n\nconst env = parseEnv(\n z.intersection(\n indexerEnvSchema,\n z.object({\n DATABASE_URL: z.string(),\n HEALTHCHECK_HOST: z.string().optional(),\n HEALTHCHECK_PORT: z.coerce.number().optional(),\n })\n )\n);\n\nconst transports: Transport[] = [\n // prefer WS when specified\n env.RPC_WS_URL ? webSocket(env.RPC_WS_URL) : undefined,\n // otherwise use or fallback to HTTP\n env.RPC_HTTP_URL ? http(env.RPC_HTTP_URL) : undefined,\n].filter(isDefined);\n\nconst publicClient = createPublicClient({\n transport: fallback(transports),\n pollingInterval: env.POLLING_INTERVAL,\n});\n\nconst chainId = await publicClient.getChainId();\nconst database = drizzle(postgres(env.DATABASE_URL, { prepare: false }));\n\nif (await shouldCleanDatabase(database, chainId)) {\n console.log(\"outdated database detected, clearing data to start fresh\");\n await cleanDatabase(database);\n}\n\nconst { storageAdapter, tables } = await createStorageAdapter({ database, publicClient });\n\nlet startBlock = env.START_BLOCK;\n\n// Resume from latest block stored in DB. This will throw if the DB doesn't exist yet, so we wrap in a try/catch and ignore the error.\n// TODO: query if the DB exists instead of try/catch\ntry {\n const chainState = await database\n .select()\n .from(tables.configTable)\n .where(eq(tables.configTable.chainId, chainId))\n .limit(1)\n .execute()\n // Get the first record in a way that returns a possible `undefined`\n // TODO: move this to `.findFirst` after upgrading drizzle or `rows[0]` after enabling `noUncheckedIndexedAccess: true`\n .then((rows) => rows.find(() => true));\n\n if (chainState?.blockNumber != null) {\n startBlock = chainState.blockNumber + 1n;\n console.log(\"resuming from block number\", startBlock);\n }\n} catch (error) {\n // ignore errors for now\n}\n\nconst { latestBlockNumber$, storedBlockLogs$ } = await createStoreSync({\n storageAdapter,\n publicClient,\n startBlock,\n maxBlockRange: env.MAX_BLOCK_RANGE,\n address: env.STORE_ADDRESS,\n});\n\nstoredBlockLogs$.subscribe();\n\nlet isCaughtUp = false;\ncombineLatest([latestBlockNumber$, storedBlockLogs$])\n .pipe(\n filter(\n ([latestBlockNumber, { blockNumber: lastBlockNumberProcessed }]) => latestBlockNumber === lastBlockNumberProcessed\n ),\n first()\n )\n .subscribe(() => {\n isCaughtUp = true;\n console.log(\"all caught up\");\n });\n\nif (env.HEALTHCHECK_HOST != null || env.HEALTHCHECK_PORT != null) {\n const { default: Koa } = await import(\"koa\");\n const { default: cors } = await import(\"@koa/cors\");\n const { default: Router } = await import(\"@koa/router\");\n\n const server = new Koa();\n server.use(cors());\n\n const router = new Router();\n\n router.get(\"/\", (ctx) => {\n ctx.body = \"emit HelloWorld();\";\n });\n\n // k8s healthchecks\n router.get(\"/healthz\", (ctx) => {\n ctx.status = 200;\n });\n router.get(\"/readyz\", (ctx) => {\n if (isCaughtUp) {\n ctx.status = 200;\n ctx.body = \"ready\";\n } else {\n ctx.status = 424;\n ctx.body = \"backfilling\";\n }\n });\n\n server.use(router.routes());\n server.use(router.allowedMethods());\n\n server.listen({ host: env.HEALTHCHECK_HOST, port: env.HEALTHCHECK_PORT });\n console.log(\n `postgres indexer healthcheck server listening on http://${env.HEALTHCHECK_HOST}:${env.HEALTHCHECK_PORT}`\n );\n}\n"],"mappings":";gDACA,MAAO,gBACP,OAAS,KAAAA,MAAS,MAClB,OAAS,MAAAC,MAAU,cACnB,OAAS,sBAAAC,EAAoB,YAAAC,EAAU,aAAAC,EAAW,QAAAC,MAAuB,OACzE,OAAS,aAAAC,MAAiB,2BAC1B,OAAS,iBAAAC,EAAe,UAAAC,EAAQ,SAAAC,MAAa,OAC7C,OAAS,WAAAC,MAAe,0BACxB,OAAOC,MAAc,WACrB,OAAS,iBAAAC,EAAe,wBAAAC,EAAsB,uBAAAC,MAA2B,kCACzE,OAAS,mBAAAC,MAAuB,yBAGhC,IAAMC,EAAMC,EACVC,EAAE,aACAC,EACAD,EAAE,OAAO,CACP,aAAcA,EAAE,OAAO,EACvB,iBAAkBA,EAAE,OAAO,EAAE,SAAS,EACtC,iBAAkBA,EAAE,OAAO,OAAO,EAAE,SAAS,CAC/C,CAAC,CACH,CACF,EAEME,EAA0B,CAE9BJ,EAAI,WAAaK,EAAUL,EAAI,UAAU,EAAI,OAE7CA,EAAI,aAAeM,EAAKN,EAAI,YAAY,EAAI,MAC9C,EAAE,OAAOO,CAAS,EAEZC,EAAeC,EAAmB,CACtC,UAAWC,EAASN,CAAU,EAC9B,gBAAiBJ,EAAI,gBACvB,CAAC,EAEKW,EAAU,MAAMH,EAAa,WAAW,EACxCI,EAAWC,EAAQC,EAASd,EAAI,aAAc,CAAE,QAAS,EAAM,CAAC,CAAC,EAEnE,MAAMe,EAAoBH,EAAUD,CAAO,IAC7C,QAAQ,IAAI,0DAA0D,EACtE,MAAMK,EAAcJ,CAAQ,GAG9B,GAAM,CAAE,eAAAK,EAAgB,OAAAC,CAAO,EAAI,MAAMC,EAAqB,CAAE,SAAAP,EAAU,aAAAJ,CAAa,CAAC,EAEpFY,EAAapB,EAAI,YAIrB,GAAI,CACF,IAAMqB,EAAa,MAAMT,EACtB,OAAO,EACP,KAAKM,EAAO,WAAW,EACvB,MAAMI,EAAGJ,EAAO,YAAY,QAASP,CAAO,CAAC,EAC7C,MAAM,CAAC,EACP,QAAQ,EAGR,KAAMY,GAASA,EAAK,KAAK,IAAM,EAAI,CAAC,EAEnCF,GAAY,aAAe,OAC7BD,EAAaC,EAAW,YAAc,GACtC,QAAQ,IAAI,6BAA8BD,CAAU,EAExD,MAAE,CAEF,CAEA,GAAM,CAAE,mBAAAI,EAAoB,iBAAAC,CAAiB,EAAI,MAAMC,EAAgB,CACrE,eAAAT,EACA,aAAAT,EACA,WAAAY,EACA,cAAepB,EAAI,gBACnB,QAASA,EAAI,aACf,CAAC,EAEDyB,EAAiB,UAAU,EAE3B,IAAIE,EAAa,GACjBC,EAAc,CAACJ,EAAoBC,CAAgB,CAAC,EACjD,KACCI,EACE,CAAC,CAACC,EAAmB,CAAE,YAAaC,CAAyB,CAAC,IAAMD,IAAsBC,CAC5F,EACAC,EAAM,CACR,EACC,UAAU,IAAM,CACfL,EAAa,GACb,QAAQ,IAAI,eAAe,CAC7B,CAAC,EAEH,GAAI3B,EAAI,kBAAoB,MAAQA,EAAI,kBAAoB,KAAM,CAChE,GAAM,CAAE,QAASiC,CAAI,EAAI,KAAM,QAAO,KAAK,EACrC,CAAE,QAASC,CAAK,EAAI,KAAM,QAAO,WAAW,EAC5C,CAAE,QAASC,CAAO,EAAI,KAAM,QAAO,aAAa,EAEhDC,EAAS,IAAIH,EACnBG,EAAO,IAAIF,EAAK,CAAC,EAEjB,IAAMG,EAAS,IAAIF,EAEnBE,EAAO,IAAI,IAAMC,GAAQ,CACvBA,EAAI,KAAO,oBACb,CAAC,EAGDD,EAAO,IAAI,WAAaC,GAAQ,CAC9BA,EAAI,OAAS,GACf,CAAC,EACDD,EAAO,IAAI,UAAYC,GAAQ,CACzBX,GACFW,EAAI,OAAS,IACbA,EAAI,KAAO,UAEXA,EAAI,OAAS,IACbA,EAAI,KAAO,cAEf,CAAC,EAEDF,EAAO,IAAIC,EAAO,OAAO,CAAC,EAC1BD,EAAO,IAAIC,EAAO,eAAe,CAAC,EAElCD,EAAO,OAAO,CAAE,KAAMpC,EAAI,iBAAkB,KAAMA,EAAI,gBAAiB,CAAC,EACxE,QAAQ,IACN,2DAA2DA,EAAI,oBAAoBA,EAAI,kBACzF","names":["z","eq","createPublicClient","fallback","webSocket","http","isDefined","combineLatest","filter","first","drizzle","postgres","cleanDatabase","createStorageAdapter","shouldCleanDatabase","createStoreSync","env","parseEnv","z","indexerEnvSchema","transports","webSocket","http","isDefined","publicClient","createPublicClient","fallback","chainId","database","drizzle","postgres","shouldCleanDatabase","cleanDatabase","storageAdapter","tables","createStorageAdapter","startBlock","chainState","eq","rows","latestBlockNumber$","storedBlockLogs$","createStoreSync","isCaughtUp","combineLatest","filter","latestBlockNumber","lastBlockNumberProcessed","first","Koa","cors","Router","server","router","ctx"]}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{a as y}from"../chunk-LOFAZSVJ.js";import{a as g,b as S,c as T}from"../chunk-2E5MDUA2.js";import"dotenv/config";import q from"node:fs";import{z as c}from"zod";import{eq as x}from"drizzle-orm";import{drizzle as D}from"drizzle-orm/better-sqlite3";import j from"better-sqlite3";import{createPublicClient as z,fallback as F,webSocket as H,http as M}from"viem";import V from"fastify";import{fastifyTRPCPlugin as $}from"@trpc/server/adapters/fastify";import{createAppRouter as W}from"@latticexyz/store-sync/trpc-indexer";import{chainState as _,schemaVersion as E,syncToSqlite as G}from"@latticexyz/store-sync/sqlite";import{eq as k}from"drizzle-orm";import{buildTable as v,chainState as R,getTables as Q}from"@latticexyz/store-sync/sqlite";import{getAddress as A}from"viem";import{decodeDynamicField as U}from"@latticexyz/protocol-parser";async function L(o){return{async findAll({chainId:d,address:l,filters:m=[]}){let u=Array.from(new Set(m.map(r=>r.tableId))),N=Q(o).filter(r=>l==null||A(l)===A(r.address)).filter(r=>!u.length||u.includes(r.tableId)).map(r=>{let b=v(r),h=o.select().from(b).where(k(b.__isDeleted,!1)).all(),w=m.length?h.filter(i=>{let n=U("bytes32[]",i.__key);return m.some(a=>a.tableId===r.tableId&&(a.key0==null||a.key0===n[0])&&(a.key1==null||a.key1===n[1]))}):h;return{...r,records:w.map(i=>({key:Object.fromEntries(Object.entries(r.keySchema).map(([n])=>[n,i[n]])),value:Object.fromEntries(Object.entries(r.valueSchema).map(([n])=>[n,i[n]]))}))}}),O=o.select().from(R).where(k(R.chainId,d)).all(),{lastUpdatedBlockNumber:B}=O[0]??{},f={blockNumber:B??null,tables:N};return y("findAll",d,l,f),f}}}import{isDefined as K}from"@latticexyz/common/utils";import{combineLatest as X,filter as J,first as Y}from"rxjs";var t=T(c.intersection(c.intersection(S,g),c.object({SQLITE_FILENAME:c.string().default("indexer.db")}))),Z=[t.RPC_WS_URL?H(t.RPC_WS_URL):void 0,t.RPC_HTTP_URL?M(t.RPC_HTTP_URL):void 0].filter(K),I=z({transport:F(Z),pollingInterval:t.POLLING_INTERVAL}),ee=await I.getChainId(),p=D(new j(t.SQLITE_FILENAME)),C=t.START_BLOCK;try{let e=p.select().from(_).where(x(_.chainId,ee)).all()[0];e!=null&&(e.schemaVersion!=E?(console.log("schema version changed from",e.schemaVersion,"to",E,"recreating database"),q.truncateSync(t.SQLITE_FILENAME)):e.lastUpdatedBlockNumber!=null&&(console.log("resuming from block number",e.lastUpdatedBlockNumber+1n),C=e.lastUpdatedBlockNumber+1n))}catch{}var{latestBlockNumber$:te,storedBlockLogs$:re}=await G({database:p,publicClient:I,startBlock:C,maxBlockRange:t.MAX_BLOCK_RANGE,address:t.STORE_ADDRESS}),P=!1;X([te,re]).pipe(J(([o,{blockNumber:e}])=>o===e),Y()).subscribe(()=>{P=!0,console.log("all caught up")});var s=V({maxParamLength:5e3});await s.register(import("@fastify/cors"));s.get("/healthz",(o,e)=>e.code(200).send());s.get("/readyz",(o,e)=>P?e.code(200).send("ready"):e.code(424).send("backfilling"));s.register($,{prefix:"/trpc",trpcOptions:{router:W(),createContext:async()=>({queryAdapter:await L(p)})}});await s.listen({host:t.HOST,port:t.PORT});console.log(`sqlite indexer frontend listening on http://${t.HOST}:${t.PORT}`);
2
+ import{a as g,c as L,d as _}from"../chunk-C47XPAJP.js";import{a as R,b as k,c as N}from"../chunk-2E5MDUA2.js";import"dotenv/config";import $ from"node:fs";import{z as p}from"zod";import{eq as G}from"drizzle-orm";import{drizzle as Y}from"drizzle-orm/better-sqlite3";import x from"better-sqlite3";import{createPublicClient as X,fallback as Z,webSocket as ee,http as te}from"viem";import re from"koa";import oe from"@koa/cors";import se from"@koa/router";import{createKoaMiddleware as ae}from"trpc-koa-adapter";import{createAppRouter as ne}from"@latticexyz/store-sync/trpc-indexer";import{chainState as O,schemaVersion as C,syncToSqlite as ie}from"@latticexyz/store-sync/sqlite";import{asc as P,eq as I}from"drizzle-orm";import{buildTable as W,chainState as w,getTables as q}from"@latticexyz/store-sync/sqlite";import{getAddress as E}from"viem";import{decodeDynamicField as U}from"@latticexyz/protocol-parser";function d(e,{chainId:t,address:r,filters:n=[]}){let i=e.select().from(w).where(I(w.chainId,t)).limit(1).all().find(()=>!0),s=Array.from(new Set(n.map(a=>a.tableId))),y=q(e).filter(a=>r==null||E(r)===E(a.address)).filter(a=>!s.length||s.includes(a.tableId)).map(a=>{let h=W(a),T=e.select().from(h).where(I(h.__isDeleted,!1)).orderBy(P(h.__lastUpdatedBlockNumber)).all(),M=n.length?T.filter(f=>{let l=U("bytes32[]",f.__key);return n.some(m=>m.tableId===a.tableId&&(m.key0==null||m.key0===l[0])&&(m.key1==null||m.key1===l[1]))}):T;return{...a,records:M.map(f=>({key:Object.fromEntries(Object.entries(a.keySchema).map(([l])=>[l,f[l]])),value:Object.fromEntries(Object.entries(a.valueSchema).map(([l])=>[l,f[l]]))}))}});return{blockNumber:i?.lastUpdatedBlockNumber??null,tables:y}}import{tablesWithRecordsToLogs as H}from"@latticexyz/store-sync";async function B(e){return{async getLogs(r){let{blockNumber:n,tables:i}=d(e,r),s=H(i);return{blockNumber:n??0n,logs:s}},async findAll(r){return d(e,r)}}}import{isDefined as le}from"@latticexyz/common/utils";import{combineLatest as ce,filter as me,first as de}from"rxjs";import F from"@koa/router";import j from"koa-compose";import{input as z}from"@latticexyz/store-sync/indexer-client";import{storeTables as J,tablesWithRecordsToLogs as K}from"@latticexyz/store-sync";import{createBenchmark as V}from"@latticexyz/common";function A(e){let t=new F;return t.get("/api/logs",L(),async r=>{let n=V("sqlite:logs"),i;try{i=z.parse(typeof r.query.input=="string"?JSON.parse(r.query.input):{})}catch(s){r.status=400,r.body=JSON.stringify(s),g(s);return}try{i.filters=i.filters.length>0?[...i.filters,{tableId:J.Tables.tableId}]:[],n("parse config");let{blockNumber:s,tables:S}=d(e,i);n("query tables with records");let y=K(S);n("convert records to logs"),r.body=JSON.stringify({blockNumber:s?.toString()??"-1",logs:y}),r.status=200}catch(s){r.status=500,r.body=JSON.stringify(s),g(s)}}),j([t.routes(),t.allowedMethods()])}var o=N(p.intersection(p.intersection(k,R),p.object({SQLITE_FILENAME:p.string().default("indexer.db"),SENTRY_DSN:p.string().optional()}))),pe=[o.RPC_WS_URL?ee(o.RPC_WS_URL):void 0,o.RPC_HTTP_URL?te(o.RPC_HTTP_URL):void 0].filter(le),D=X({transport:Z(pe),pollingInterval:o.POLLING_INTERVAL}),ue=await D.getChainId(),b=Y(new x(o.SQLITE_FILENAME)),Q=o.START_BLOCK;try{let t=b.select().from(O).where(G(O.chainId,ue)).all()[0];t!=null&&(t.schemaVersion!=C?(console.log("schema version changed from",t.schemaVersion,"to",C,"recreating database"),$.truncateSync(o.SQLITE_FILENAME)):t.lastUpdatedBlockNumber!=null&&(console.log("resuming from block number",t.lastUpdatedBlockNumber+1n),Q=t.lastUpdatedBlockNumber+1n))}catch{}var{latestBlockNumber$:fe,storedBlockLogs$:be}=await ie({database:b,publicClient:D,startBlock:Q,maxBlockRange:o.MAX_BLOCK_RANGE,address:o.STORE_ADDRESS}),v=!1;ce([fe,be]).pipe(me(([e,{blockNumber:t}])=>e===t),de()).subscribe(()=>{v=!0,console.log("all caught up")});var c=new re;c.use(oe());c.use(A(b));o.SENTRY_DSN&&_(c);var u=new se;u.get("/",e=>{e.body="emit HelloWorld();"});u.get("/healthz",e=>{e.status=200});u.get("/readyz",e=>{v?(e.status=200,e.body="ready"):(e.status=424,e.body="backfilling")});c.use(u.routes());c.use(u.allowedMethods());c.use(ae({prefix:"/trpc",router:ne(),createContext:async()=>({queryAdapter:await B(b)})}));c.listen({host:o.HOST,port:o.PORT});console.log(`sqlite indexer frontend listening on http://${o.HOST}:${o.PORT}`);
3
3
  //# sourceMappingURL=sqlite-indexer.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../bin/sqlite-indexer.ts","../../src/sqlite/createQueryAdapter.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport fs from \"node:fs\";\nimport { z } from \"zod\";\nimport { eq } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport Database from \"better-sqlite3\";\nimport { createPublicClient, fallback, webSocket, http, Transport } from \"viem\";\nimport fastify from \"fastify\";\nimport { fastifyTRPCPlugin } from \"@trpc/server/adapters/fastify\";\nimport { AppRouter, createAppRouter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { chainState, schemaVersion, syncToSqlite } from \"@latticexyz/store-sync/sqlite\";\nimport { createQueryAdapter } from \"../src/sqlite/createQueryAdapter\";\nimport { isDefined } from \"@latticexyz/common/utils\";\nimport { combineLatest, filter, first } from \"rxjs\";\nimport { frontendEnvSchema, indexerEnvSchema, parseEnv } from \"./parseEnv\";\n\nconst env = parseEnv(\n z.intersection(\n z.intersection(indexerEnvSchema, frontendEnvSchema),\n z.object({\n SQLITE_FILENAME: z.string().default(\"indexer.db\"),\n })\n )\n);\n\nconst transports: Transport[] = [\n // prefer WS when specified\n env.RPC_WS_URL ? webSocket(env.RPC_WS_URL) : undefined,\n // otherwise use or fallback to HTTP\n env.RPC_HTTP_URL ? http(env.RPC_HTTP_URL) : undefined,\n].filter(isDefined);\n\nconst publicClient = createPublicClient({\n transport: fallback(transports),\n pollingInterval: env.POLLING_INTERVAL,\n});\n\nconst chainId = await publicClient.getChainId();\nconst database = drizzle(new Database(env.SQLITE_FILENAME));\n\nlet startBlock = env.START_BLOCK;\n\n// Resume from latest block stored in DB. This will throw if the DB doesn't exist yet, so we wrap in a try/catch and ignore the error.\ntry {\n const currentChainStates = database.select().from(chainState).where(eq(chainState.chainId, chainId)).all();\n // TODO: replace this type workaround with `noUncheckedIndexedAccess: true` when we can fix all the issues related (https://github.com/latticexyz/mud/issues/1212)\n const currentChainState: (typeof currentChainStates)[number] | undefined = currentChainStates[0];\n\n if (currentChainState != null) {\n if (currentChainState.schemaVersion != schemaVersion) {\n console.log(\n \"schema version changed from\",\n currentChainState.schemaVersion,\n \"to\",\n schemaVersion,\n \"recreating database\"\n );\n fs.truncateSync(env.SQLITE_FILENAME);\n } else if (currentChainState.lastUpdatedBlockNumber != null) {\n console.log(\"resuming from block number\", currentChainState.lastUpdatedBlockNumber + 1n);\n startBlock = currentChainState.lastUpdatedBlockNumber + 1n;\n }\n }\n} catch (error) {\n // ignore errors, this is optional\n}\n\nconst { latestBlockNumber$, storedBlockLogs$ } = await syncToSqlite({\n database,\n publicClient,\n startBlock,\n maxBlockRange: env.MAX_BLOCK_RANGE,\n address: env.STORE_ADDRESS,\n});\n\nlet isCaughtUp = false;\ncombineLatest([latestBlockNumber$, storedBlockLogs$])\n .pipe(\n filter(\n ([latestBlockNumber, { blockNumber: lastBlockNumberProcessed }]) => latestBlockNumber === lastBlockNumberProcessed\n ),\n first()\n )\n .subscribe(() => {\n isCaughtUp = true;\n console.log(\"all caught up\");\n });\n\n// @see https://fastify.dev/docs/latest/\nconst server = fastify({\n maxParamLength: 5000,\n});\n\nawait server.register(import(\"@fastify/cors\"));\n\n// k8s healthchecks\nserver.get(\"/healthz\", (req, res) => res.code(200).send());\nserver.get(\"/readyz\", (req, res) => (isCaughtUp ? res.code(200).send(\"ready\") : res.code(424).send(\"backfilling\")));\n\n// @see https://trpc.io/docs/server/adapters/fastify\nserver.register(fastifyTRPCPlugin<AppRouter>, {\n prefix: \"/trpc\",\n trpcOptions: {\n router: createAppRouter(),\n createContext: async () => ({\n queryAdapter: await createQueryAdapter(database),\n }),\n },\n});\n\nawait server.listen({ host: env.HOST, port: env.PORT });\nconsole.log(`sqlite indexer frontend listening on http://${env.HOST}:${env.PORT}`);\n","import { eq } from \"drizzle-orm\";\nimport { BaseSQLiteDatabase } from \"drizzle-orm/sqlite-core\";\nimport { buildTable, chainState, getTables } from \"@latticexyz/store-sync/sqlite\";\nimport { QueryAdapter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { debug } from \"../debug\";\nimport { getAddress } from \"viem\";\nimport { decodeDynamicField } from \"@latticexyz/protocol-parser\";\n\n/**\n * Creates a storage adapter for the tRPC server/client to query data from SQLite.\n *\n * @param {BaseSQLiteDatabase<\"sync\", any>} database SQLite database object from Drizzle\n * @returns {Promise<QueryAdapter>} A set of methods used by tRPC endpoints.\n */\nexport async function createQueryAdapter(database: BaseSQLiteDatabase<\"sync\", any>): Promise<QueryAdapter> {\n const adapter: QueryAdapter = {\n async findAll({ chainId, address, filters = [] }) {\n // If _any_ filter has a table ID, this will filter down all data to just those tables. Which mean we can't yet mix table filters with key-only filters.\n // TODO: improve this so we can express this in the query (need to be able to query data across tables more easily)\n const tableIds = Array.from(new Set(filters.map((filter) => filter.tableId)));\n const tables = getTables(database)\n .filter((table) => address == null || getAddress(address) === getAddress(table.address))\n .filter((table) => !tableIds.length || tableIds.includes(table.tableId));\n\n const tablesWithRecords = tables.map((table) => {\n const sqliteTable = buildTable(table);\n const records = database.select().from(sqliteTable).where(eq(sqliteTable.__isDeleted, false)).all();\n const filteredRecords = !filters.length\n ? records\n : records.filter((record) => {\n const keyTuple = decodeDynamicField(\"bytes32[]\", record.__key);\n return filters.some(\n (filter) =>\n filter.tableId === table.tableId &&\n (filter.key0 == null || filter.key0 === keyTuple[0]) &&\n (filter.key1 == null || filter.key1 === keyTuple[1])\n );\n });\n return {\n ...table,\n records: filteredRecords.map((record) => ({\n key: Object.fromEntries(Object.entries(table.keySchema).map(([name]) => [name, record[name]])),\n value: Object.fromEntries(Object.entries(table.valueSchema).map(([name]) => [name, record[name]])),\n })),\n };\n });\n\n const metadata = database.select().from(chainState).where(eq(chainState.chainId, chainId)).all();\n const { lastUpdatedBlockNumber } = metadata[0] ?? {};\n\n const result = {\n blockNumber: lastUpdatedBlockNumber ?? null,\n tables: tablesWithRecords,\n };\n\n debug(\"findAll\", chainId, address, result);\n\n return result;\n },\n };\n return adapter;\n}\n"],"mappings":"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","names":["fs","z","eq","drizzle","Database","createPublicClient","fallback","webSocket","http","fastify","fastifyTRPCPlugin","createAppRouter","chainState","schemaVersion","syncToSqlite","eq","buildTable","chainState","getTables","getAddress","decodeDynamicField","createQueryAdapter","database","chainId","address","filters","tableIds","filter","tablesWithRecords","getTables","table","sqliteTable","buildTable","records","eq","filteredRecords","record","keyTuple","name","metadata","chainState","lastUpdatedBlockNumber","result","debug","isDefined","combineLatest","filter","first","env","parseEnv","z","indexerEnvSchema","frontendEnvSchema","transports","webSocket","http","isDefined","publicClient","createPublicClient","fallback","chainId","database","drizzle","Database","startBlock","currentChainState","chainState","eq","schemaVersion","fs","latestBlockNumber$","storedBlockLogs$","syncToSqlite","isCaughtUp","combineLatest","filter","latestBlockNumber","lastBlockNumberProcessed","first","server","fastify","req","res","fastifyTRPCPlugin","createAppRouter","createQueryAdapter"]}
1
+ {"version":3,"sources":["../../bin/sqlite-indexer.ts","../../src/sqlite/getTablesWithRecords.ts","../../src/sqlite/createQueryAdapter.ts","../../src/sqlite/apiRoutes.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport fs from \"node:fs\";\nimport { z } from \"zod\";\nimport { eq } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport Database from \"better-sqlite3\";\nimport { createPublicClient, fallback, webSocket, http, Transport } from \"viem\";\nimport Koa from \"koa\";\nimport cors from \"@koa/cors\";\nimport Router from \"@koa/router\";\nimport { createKoaMiddleware } from \"trpc-koa-adapter\";\nimport { createAppRouter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { chainState, schemaVersion, syncToSqlite } from \"@latticexyz/store-sync/sqlite\";\nimport { createQueryAdapter } from \"../src/sqlite/createQueryAdapter\";\nimport { isDefined } from \"@latticexyz/common/utils\";\nimport { combineLatest, filter, first } from \"rxjs\";\nimport { frontendEnvSchema, indexerEnvSchema, parseEnv } from \"./parseEnv\";\nimport { apiRoutes } from \"../src/sqlite/apiRoutes\";\nimport { registerSentryMiddlewares } from \"../src/sentry\";\n\nconst env = parseEnv(\n z.intersection(\n z.intersection(indexerEnvSchema, frontendEnvSchema),\n z.object({\n SQLITE_FILENAME: z.string().default(\"indexer.db\"),\n SENTRY_DSN: z.string().optional(),\n })\n )\n);\n\nconst transports: Transport[] = [\n // prefer WS when specified\n env.RPC_WS_URL ? webSocket(env.RPC_WS_URL) : undefined,\n // otherwise use or fallback to HTTP\n env.RPC_HTTP_URL ? http(env.RPC_HTTP_URL) : undefined,\n].filter(isDefined);\n\nconst publicClient = createPublicClient({\n transport: fallback(transports),\n pollingInterval: env.POLLING_INTERVAL,\n});\n\nconst chainId = await publicClient.getChainId();\nconst database = drizzle(new Database(env.SQLITE_FILENAME));\n\nlet startBlock = env.START_BLOCK;\n\n// Resume from latest block stored in DB. This will throw if the DB doesn't exist yet, so we wrap in a try/catch and ignore the error.\ntry {\n const currentChainStates = database.select().from(chainState).where(eq(chainState.chainId, chainId)).all();\n // TODO: replace this type workaround with `noUncheckedIndexedAccess: true` when we can fix all the issues related (https://github.com/latticexyz/mud/issues/1212)\n const currentChainState: (typeof currentChainStates)[number] | undefined = currentChainStates[0];\n\n if (currentChainState != null) {\n if (currentChainState.schemaVersion != schemaVersion) {\n console.log(\n \"schema version changed from\",\n currentChainState.schemaVersion,\n \"to\",\n schemaVersion,\n \"recreating database\"\n );\n fs.truncateSync(env.SQLITE_FILENAME);\n } else if (currentChainState.lastUpdatedBlockNumber != null) {\n console.log(\"resuming from block number\", currentChainState.lastUpdatedBlockNumber + 1n);\n startBlock = currentChainState.lastUpdatedBlockNumber + 1n;\n }\n }\n} catch (error) {\n // ignore errors, this is optional\n}\n\nconst { latestBlockNumber$, storedBlockLogs$ } = await syncToSqlite({\n database,\n publicClient,\n startBlock,\n maxBlockRange: env.MAX_BLOCK_RANGE,\n address: env.STORE_ADDRESS,\n});\n\nlet isCaughtUp = false;\ncombineLatest([latestBlockNumber$, storedBlockLogs$])\n .pipe(\n filter(\n ([latestBlockNumber, { blockNumber: lastBlockNumberProcessed }]) => latestBlockNumber === lastBlockNumberProcessed\n ),\n first()\n )\n .subscribe(() => {\n isCaughtUp = true;\n console.log(\"all caught up\");\n });\n\nconst server = new Koa();\nserver.use(cors());\nserver.use(apiRoutes(database));\n\nif (env.SENTRY_DSN) {\n registerSentryMiddlewares(server);\n}\n\nconst router = new Router();\n\nrouter.get(\"/\", (ctx) => {\n ctx.body = \"emit HelloWorld();\";\n});\n\n// k8s healthchecks\nrouter.get(\"/healthz\", (ctx) => {\n ctx.status = 200;\n});\nrouter.get(\"/readyz\", (ctx) => {\n if (isCaughtUp) {\n ctx.status = 200;\n ctx.body = \"ready\";\n } else {\n ctx.status = 424;\n ctx.body = \"backfilling\";\n }\n});\n\nserver.use(router.routes());\nserver.use(router.allowedMethods());\n\nserver.use(\n createKoaMiddleware({\n prefix: \"/trpc\",\n router: createAppRouter(),\n createContext: async () => ({\n queryAdapter: await createQueryAdapter(database),\n }),\n })\n);\n\nserver.listen({ host: env.HOST, port: env.PORT });\nconsole.log(`sqlite indexer frontend listening on http://${env.HOST}:${env.PORT}`);\n","import { asc, eq } from \"drizzle-orm\";\nimport { BaseSQLiteDatabase } from \"drizzle-orm/sqlite-core\";\nimport { buildTable, chainState, getTables } from \"@latticexyz/store-sync/sqlite\";\nimport { Hex, getAddress } from \"viem\";\nimport { decodeDynamicField } from \"@latticexyz/protocol-parser\";\nimport { SyncFilter, TableWithRecords } from \"@latticexyz/store-sync\";\n\n// TODO: refactor sqlite and replace this with getLogs to match postgres (https://github.com/latticexyz/mud/issues/1970)\n\n/**\n * @deprecated\n * */\nexport function getTablesWithRecords(\n database: BaseSQLiteDatabase<\"sync\", any>,\n {\n chainId,\n address,\n filters = [],\n }: {\n readonly chainId: number;\n readonly address?: Hex;\n readonly filters?: readonly SyncFilter[];\n }\n): { blockNumber: bigint | null; tables: readonly TableWithRecords[] } {\n const metadata = database\n .select()\n .from(chainState)\n .where(eq(chainState.chainId, chainId))\n .limit(1)\n .all()\n .find(() => true);\n\n // If _any_ filter has a table ID, this will filter down all data to just those tables. Which mean we can't yet mix table filters with key-only filters.\n // TODO: improve this so we can express this in the query (need to be able to query data across tables more easily)\n const tableIds = Array.from(new Set(filters.map((filter) => filter.tableId)));\n const tables = getTables(database)\n .filter((table) => address == null || getAddress(address) === getAddress(table.address))\n .filter((table) => !tableIds.length || tableIds.includes(table.tableId));\n\n const tablesWithRecords = tables.map((table) => {\n const sqliteTable = buildTable(table);\n const records = database\n .select()\n .from(sqliteTable)\n .where(eq(sqliteTable.__isDeleted, false))\n .orderBy(\n asc(sqliteTable.__lastUpdatedBlockNumber)\n // TODO: add logIndex (https://github.com/latticexyz/mud/issues/1979)\n )\n .all();\n const filteredRecords = !filters.length\n ? records\n : records.filter((record) => {\n const keyTuple = decodeDynamicField(\"bytes32[]\", record.__key);\n return filters.some(\n (filter) =>\n filter.tableId === table.tableId &&\n (filter.key0 == null || filter.key0 === keyTuple[0]) &&\n (filter.key1 == null || filter.key1 === keyTuple[1])\n );\n });\n return {\n ...table,\n records: filteredRecords.map((record) => ({\n key: Object.fromEntries(Object.entries(table.keySchema).map(([name]) => [name, record[name]])),\n value: Object.fromEntries(Object.entries(table.valueSchema).map(([name]) => [name, record[name]])),\n })),\n };\n });\n\n return {\n blockNumber: metadata?.lastUpdatedBlockNumber ?? null,\n tables: tablesWithRecords,\n };\n}\n","import { BaseSQLiteDatabase } from \"drizzle-orm/sqlite-core\";\nimport { QueryAdapter } from \"@latticexyz/store-sync/trpc-indexer\";\nimport { getTablesWithRecords } from \"./getTablesWithRecords\";\nimport { tablesWithRecordsToLogs } from \"@latticexyz/store-sync\";\n\n/**\n * Creates a storage adapter for the tRPC server/client to query data from SQLite.\n *\n * @param {BaseSQLiteDatabase<\"sync\", any>} database SQLite database object from Drizzle\n * @returns {Promise<QueryAdapter>} A set of methods used by tRPC endpoints.\n */\nexport async function createQueryAdapter(database: BaseSQLiteDatabase<\"sync\", any>): Promise<QueryAdapter> {\n const adapter: QueryAdapter = {\n async getLogs(opts) {\n const { blockNumber, tables } = getTablesWithRecords(database, opts);\n const logs = tablesWithRecordsToLogs(tables);\n return { blockNumber: blockNumber ?? 0n, logs };\n },\n async findAll(opts) {\n return getTablesWithRecords(database, opts);\n },\n };\n return adapter;\n}\n","import { Middleware } from \"koa\";\nimport Router from \"@koa/router\";\nimport compose from \"koa-compose\";\nimport { input } from \"@latticexyz/store-sync/indexer-client\";\nimport { storeTables, tablesWithRecordsToLogs } from \"@latticexyz/store-sync\";\nimport { debug } from \"../debug\";\nimport { createBenchmark } from \"@latticexyz/common\";\nimport { compress } from \"../compress\";\nimport { getTablesWithRecords } from \"./getTablesWithRecords\";\nimport { BaseSQLiteDatabase } from \"drizzle-orm/sqlite-core\";\n\nexport function apiRoutes(database: BaseSQLiteDatabase<\"sync\", any>): Middleware {\n const router = new Router();\n\n router.get(\"/api/logs\", compress(), async (ctx) => {\n const benchmark = createBenchmark(\"sqlite:logs\");\n\n let options: ReturnType<typeof input.parse>;\n\n try {\n options = input.parse(typeof ctx.query.input === \"string\" ? JSON.parse(ctx.query.input) : {});\n } catch (error) {\n ctx.status = 400;\n ctx.body = JSON.stringify(error);\n debug(error);\n return;\n }\n\n try {\n options.filters = options.filters.length > 0 ? [...options.filters, { tableId: storeTables.Tables.tableId }] : [];\n benchmark(\"parse config\");\n const { blockNumber, tables } = getTablesWithRecords(database, options);\n benchmark(\"query tables with records\");\n const logs = tablesWithRecordsToLogs(tables);\n benchmark(\"convert records to logs\");\n\n ctx.body = JSON.stringify({ blockNumber: blockNumber?.toString() ?? \"-1\", logs });\n ctx.status = 200;\n } catch (error) {\n ctx.status = 500;\n ctx.body = JSON.stringify(error);\n debug(error);\n }\n });\n\n return compose([router.routes(), router.allowedMethods()]) as Middleware;\n}\n"],"mappings":";8GACA,MAAO,gBACP,OAAOA,MAAQ,UACf,OAAS,KAAAC,MAAS,MAClB,OAAS,MAAAC,MAAU,cACnB,OAAS,WAAAC,MAAe,6BACxB,OAAOC,MAAc,iBACrB,OAAS,sBAAAC,EAAoB,YAAAC,EAAU,aAAAC,GAAW,QAAAC,OAAuB,OACzE,OAAOC,OAAS,MAChB,OAAOC,OAAU,YACjB,OAAOC,OAAY,cACnB,OAAS,uBAAAC,OAA2B,mBACpC,OAAS,mBAAAC,OAAuB,sCAChC,OAAS,cAAAC,EAAY,iBAAAC,EAAe,gBAAAC,OAAoB,gCCbxD,OAAS,OAAAC,EAAK,MAAAC,MAAU,cAExB,OAAS,cAAAC,EAAY,cAAAC,EAAY,aAAAC,MAAiB,gCAClD,OAAc,cAAAC,MAAkB,OAChC,OAAS,sBAAAC,MAA0B,8BAQ5B,SAASC,EACdC,EACA,CACE,QAAAC,EACA,QAAAC,EACA,QAAAC,EAAU,CAAC,CACb,EAKqE,CACrE,IAAMC,EAAWJ,EACd,OAAO,EACP,KAAKL,CAAU,EACf,MAAMF,EAAGE,EAAW,QAASM,CAAO,CAAC,EACrC,MAAM,CAAC,EACP,IAAI,EACJ,KAAK,IAAM,EAAI,EAIZI,EAAW,MAAM,KAAK,IAAI,IAAIF,EAAQ,IAAKG,GAAWA,EAAO,OAAO,CAAC,CAAC,EAKtEC,EAJSX,EAAUI,CAAQ,EAC9B,OAAQQ,GAAUN,GAAW,MAAQL,EAAWK,CAAO,IAAML,EAAWW,EAAM,OAAO,CAAC,EACtF,OAAQA,GAAU,CAACH,EAAS,QAAUA,EAAS,SAASG,EAAM,OAAO,CAAC,EAExC,IAAKA,GAAU,CAC9C,IAAMC,EAAcf,EAAWc,CAAK,EAC9BE,EAAUV,EACb,OAAO,EACP,KAAKS,CAAW,EAChB,MAAMhB,EAAGgB,EAAY,YAAa,EAAK,CAAC,EACxC,QACCjB,EAAIiB,EAAY,wBAAwB,CAE1C,EACC,IAAI,EACDE,EAAmBR,EAAQ,OAE7BO,EAAQ,OAAQE,GAAW,CACzB,IAAMC,EAAWf,EAAmB,YAAac,EAAO,KAAK,EAC7D,OAAOT,EAAQ,KACZG,GACCA,EAAO,UAAYE,EAAM,UACxBF,EAAO,MAAQ,MAAQA,EAAO,OAASO,EAAS,CAAC,KACjDP,EAAO,MAAQ,MAAQA,EAAO,OAASO,EAAS,CAAC,EACtD,CACF,CAAC,EATDH,EAUJ,MAAO,CACL,GAAGF,EACH,QAASG,EAAgB,IAAKC,IAAY,CACxC,IAAK,OAAO,YAAY,OAAO,QAAQJ,EAAM,SAAS,EAAE,IAAI,CAAC,CAACM,CAAI,IAAM,CAACA,EAAMF,EAAOE,CAAI,CAAC,CAAC,CAAC,EAC7F,MAAO,OAAO,YAAY,OAAO,QAAQN,EAAM,WAAW,EAAE,IAAI,CAAC,CAACM,CAAI,IAAM,CAACA,EAAMF,EAAOE,CAAI,CAAC,CAAC,CAAC,CACnG,EAAE,CACJ,CACF,CAAC,EAED,MAAO,CACL,YAAaV,GAAU,wBAA0B,KACjD,OAAQG,CACV,CACF,CCvEA,OAAS,2BAAAQ,MAA+B,yBAQxC,eAAsBC,EAAmBC,EAAkE,CAWzG,MAV8B,CAC5B,MAAM,QAAQC,EAAM,CAClB,GAAM,CAAE,YAAAC,EAAa,OAAAC,CAAO,EAAIC,EAAqBJ,EAAUC,CAAI,EAC7DI,EAAOP,EAAwBK,CAAM,EAC3C,MAAO,CAAE,YAAaD,GAAe,GAAI,KAAAG,CAAK,CAChD,EACA,MAAM,QAAQJ,EAAM,CAClB,OAAOG,EAAqBJ,EAAUC,CAAI,CAC5C,CACF,CAEF,CFRA,OAAS,aAAAK,OAAiB,2BAC1B,OAAS,iBAAAC,GAAe,UAAAC,GAAQ,SAAAC,OAAa,OGf7C,OAAOC,MAAY,cACnB,OAAOC,MAAa,cACpB,OAAS,SAAAC,MAAa,wCACtB,OAAS,eAAAC,EAAa,2BAAAC,MAA+B,yBAErD,OAAS,mBAAAC,MAAuB,qBAKzB,SAASC,EAAUC,EAAuD,CAC/E,IAAMC,EAAS,IAAIC,EAEnB,OAAAD,EAAO,IAAI,YAAaE,EAAS,EAAG,MAAOC,GAAQ,CACjD,IAAMC,EAAYC,EAAgB,aAAa,EAE3CC,EAEJ,GAAI,CACFA,EAAUC,EAAM,MAAM,OAAOJ,EAAI,MAAM,OAAU,SAAW,KAAK,MAAMA,EAAI,MAAM,KAAK,EAAI,CAAC,CAAC,CAC9F,OAASK,EAAP,CACAL,EAAI,OAAS,IACbA,EAAI,KAAO,KAAK,UAAUK,CAAK,EAC/BC,EAAMD,CAAK,EACX,MACF,CAEA,GAAI,CACFF,EAAQ,QAAUA,EAAQ,QAAQ,OAAS,EAAI,CAAC,GAAGA,EAAQ,QAAS,CAAE,QAASI,EAAY,OAAO,OAAQ,CAAC,EAAI,CAAC,EAChHN,EAAU,cAAc,EACxB,GAAM,CAAE,YAAAO,EAAa,OAAAC,CAAO,EAAIC,EAAqBd,EAAUO,CAAO,EACtEF,EAAU,2BAA2B,EACrC,IAAMU,EAAOC,EAAwBH,CAAM,EAC3CR,EAAU,yBAAyB,EAEnCD,EAAI,KAAO,KAAK,UAAU,CAAE,YAAaQ,GAAa,SAAS,GAAK,KAAM,KAAAG,CAAK,CAAC,EAChFX,EAAI,OAAS,GACf,OAASK,EAAP,CACAL,EAAI,OAAS,IACbA,EAAI,KAAO,KAAK,UAAUK,CAAK,EAC/BC,EAAMD,CAAK,CACb,CACF,CAAC,EAEMQ,EAAQ,CAAChB,EAAO,OAAO,EAAGA,EAAO,eAAe,CAAC,CAAC,CAC3D,CHzBA,IAAMiB,EAAMC,EACVC,EAAE,aACAA,EAAE,aAAaC,EAAkBC,CAAiB,EAClDF,EAAE,OAAO,CACP,gBAAiBA,EAAE,OAAO,EAAE,QAAQ,YAAY,EAChD,WAAYA,EAAE,OAAO,EAAE,SAAS,CAClC,CAAC,CACH,CACF,EAEMG,GAA0B,CAE9BL,EAAI,WAAaM,GAAUN,EAAI,UAAU,EAAI,OAE7CA,EAAI,aAAeO,GAAKP,EAAI,YAAY,EAAI,MAC9C,EAAE,OAAOQ,EAAS,EAEZC,EAAeC,EAAmB,CACtC,UAAWC,EAASN,EAAU,EAC9B,gBAAiBL,EAAI,gBACvB,CAAC,EAEKY,GAAU,MAAMH,EAAa,WAAW,EACxCI,EAAWC,EAAQ,IAAIC,EAASf,EAAI,eAAe,CAAC,EAEtDgB,EAAahB,EAAI,YAGrB,GAAI,CAGF,IAAMiB,EAFqBJ,EAAS,OAAO,EAAE,KAAKK,CAAU,EAAE,MAAMC,EAAGD,EAAW,QAASN,EAAO,CAAC,EAAE,IAAI,EAEX,CAAC,EAE3FK,GAAqB,OACnBA,EAAkB,eAAiBG,GACrC,QAAQ,IACN,8BACAH,EAAkB,cAClB,KACAG,EACA,qBACF,EACAC,EAAG,aAAarB,EAAI,eAAe,GAC1BiB,EAAkB,wBAA0B,OACrD,QAAQ,IAAI,6BAA8BA,EAAkB,uBAAyB,EAAE,EACvFD,EAAaC,EAAkB,uBAAyB,IAG9D,MAAE,CAEF,CAEA,GAAM,CAAE,mBAAAK,GAAoB,iBAAAC,EAAiB,EAAI,MAAMC,GAAa,CAClE,SAAAX,EACA,aAAAJ,EACA,WAAAO,EACA,cAAehB,EAAI,gBACnB,QAASA,EAAI,aACf,CAAC,EAEGyB,EAAa,GACjBC,GAAc,CAACJ,GAAoBC,EAAgB,CAAC,EACjD,KACCI,GACE,CAAC,CAACC,EAAmB,CAAE,YAAaC,CAAyB,CAAC,IAAMD,IAAsBC,CAC5F,EACAC,GAAM,CACR,EACC,UAAU,IAAM,CACfL,EAAa,GACb,QAAQ,IAAI,eAAe,CAC7B,CAAC,EAEH,IAAMM,EAAS,IAAIC,GACnBD,EAAO,IAAIE,GAAK,CAAC,EACjBF,EAAO,IAAIG,EAAUrB,CAAQ,CAAC,EAE1Bb,EAAI,YACNmC,EAA0BJ,CAAM,EAGlC,IAAMK,EAAS,IAAIC,GAEnBD,EAAO,IAAI,IAAME,GAAQ,CACvBA,EAAI,KAAO,oBACb,CAAC,EAGDF,EAAO,IAAI,WAAaE,GAAQ,CAC9BA,EAAI,OAAS,GACf,CAAC,EACDF,EAAO,IAAI,UAAYE,GAAQ,CACzBb,GACFa,EAAI,OAAS,IACbA,EAAI,KAAO,UAEXA,EAAI,OAAS,IACbA,EAAI,KAAO,cAEf,CAAC,EAEDP,EAAO,IAAIK,EAAO,OAAO,CAAC,EAC1BL,EAAO,IAAIK,EAAO,eAAe,CAAC,EAElCL,EAAO,IACLQ,GAAoB,CAClB,OAAQ,QACR,OAAQC,GAAgB,EACxB,cAAe,UAAa,CAC1B,aAAc,MAAMC,EAAmB5B,CAAQ,CACjD,EACF,CAAC,CACH,EAEAkB,EAAO,OAAO,CAAE,KAAM/B,EAAI,KAAM,KAAMA,EAAI,IAAK,CAAC,EAChD,QAAQ,IAAI,+CAA+CA,EAAI,QAAQA,EAAI,MAAM","names":["fs","z","eq","drizzle","Database","createPublicClient","fallback","webSocket","http","Koa","cors","Router","createKoaMiddleware","createAppRouter","chainState","schemaVersion","syncToSqlite","asc","eq","buildTable","chainState","getTables","getAddress","decodeDynamicField","getTablesWithRecords","database","chainId","address","filters","metadata","tableIds","filter","tablesWithRecords","table","sqliteTable","records","filteredRecords","record","keyTuple","name","tablesWithRecordsToLogs","createQueryAdapter","database","opts","blockNumber","tables","getTablesWithRecords","logs","isDefined","combineLatest","filter","first","Router","compose","input","storeTables","tablesWithRecordsToLogs","createBenchmark","apiRoutes","database","router","Router","compress","ctx","benchmark","createBenchmark","options","input","error","debug","storeTables","blockNumber","tables","getTablesWithRecords","logs","tablesWithRecordsToLogs","compose","env","parseEnv","z","indexerEnvSchema","frontendEnvSchema","transports","webSocket","http","isDefined","publicClient","createPublicClient","fallback","chainId","database","drizzle","Database","startBlock","currentChainState","chainState","eq","schemaVersion","fs","latestBlockNumber$","storedBlockLogs$","syncToSqlite","isCaughtUp","combineLatest","filter","latestBlockNumber","lastBlockNumberProcessed","first","server","Koa","cors","apiRoutes","registerSentryMiddlewares","router","Router","ctx","createKoaMiddleware","createAppRouter","createQueryAdapter"]}
@@ -0,0 +1,2 @@
1
+ import*as t from"@sentry/node";import{ProfilingIntegration as l}from"@sentry/profiling-node";import{stripUrlQueryAndFragment as y}from"@sentry/utils";import c from"debug";var d=c("mud:store-indexer"),p=c("mud:store-indexer");d.log=console.debug.bind(console);p.log=console.error.bind(console);t.init({dsn:process.env.SENTRY_DSN,integrations:[...t.autoDiscoverNodePerformanceMonitoringIntegrations(),new l],tracesSampleRate:1,profilesSampleRate:1});var f=(e,a)=>new Promise((r,o)=>{t.runWithAsyncContext(async()=>{t.getCurrentHub().configureScope(n=>n.addEventProcessor(i=>t.addRequestDataToEvent(i,e.request,{include:{user:!1}})));try{await a()}catch(n){o(n)}r()})}),g=async(e,a)=>{let r=(e.method||"").toUpperCase(),o=e.url&&y(e.url),s;e.request.get("sentry-trace")&&(s=t.extractTraceparentData(e.request.get("sentry-trace")));let n=t.startTransaction({name:`${r} ${o}`,op:"http.server",...s});e.__sentry_transaction=n,t.getCurrentHub().configureScope(i=>{i.setSpan(n)}),e.res.on("finish",()=>{setImmediate(()=>{if(e._matchedRoute){let i=e.mountPath||"";n.setName(`${r} ${i}${e._matchedRoute}`)}n.setHttpStatus(e.status),n.finish()})}),await a()},S=async(e,a)=>{try{await a()}catch(r){throw t.withScope(o=>{o.addEventProcessor(s=>t.addRequestDataToEvent(s,e.request)),t.captureException(r)}),r}},_=e=>{d("Registering Sentry middlewares"),e.use(S),e.use(f),e.use(g)};import{Stream as b}from"node:stream";import h from"accepts";import{createBrotliCompress as w,createDeflate as q,createGzip as M}from"node:zlib";import{includes as R}from"@latticexyz/common/utils";var m={br:w,gzip:M,deflate:q},u=Object.keys(m);function v(e,a){let r=0;return e.on("data",o=>{r+=o.length,r>a&&(r=0,e.flush())}),e}function U({flushThreshold:e=1024*4}={}){return async function(r,o){r.vary("Accept-Encoding"),await o();let s=h(r.req).encoding(u);if(!R(u,s))return;let n=v(m[s](),e);r.set("Content-Encoding",s),r.body=r.body instanceof b?r.body.pipe(n):n.end(r.body)}}export{d as a,p as b,U as c,_ as d};
2
+ //# sourceMappingURL=chunk-C47XPAJP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sentry.ts","../src/debug.ts","../src/compress.ts"],"sourcesContent":["import * as Sentry from \"@sentry/node\";\nimport { ProfilingIntegration } from \"@sentry/profiling-node\";\nimport { stripUrlQueryAndFragment } from \"@sentry/utils\";\nimport { debug } from \"./debug\";\nimport Koa from \"koa\";\n\nSentry.init({\n dsn: process.env.SENTRY_DSN,\n integrations: [\n // Automatically instrument Node.js libraries and frameworks\n ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(),\n new ProfilingIntegration(),\n ],\n // Performance Monitoring\n tracesSampleRate: 1.0,\n // Set sampling rate for profiling - this is relative to tracesSampleRate\n profilesSampleRate: 1.0,\n});\n\nconst requestHandler: Koa.Middleware = (ctx, next) => {\n return new Promise<void>((resolve, reject) => {\n Sentry.runWithAsyncContext(async () => {\n const hub = Sentry.getCurrentHub();\n hub.configureScope((scope) =>\n scope.addEventProcessor((event) =>\n Sentry.addRequestDataToEvent(event, ctx.request, {\n include: {\n user: false,\n },\n })\n )\n );\n\n try {\n await next();\n } catch (err) {\n reject(err);\n }\n resolve();\n });\n });\n};\n\n// This tracing middleware creates a transaction per request\nconst tracingMiddleWare: Koa.Middleware = async (ctx, next) => {\n const reqMethod = (ctx.method || \"\").toUpperCase();\n const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url);\n\n // Connect to trace of upstream app\n let traceparentData;\n if (ctx.request.get(\"sentry-trace\")) {\n traceparentData = Sentry.extractTraceparentData(ctx.request.get(\"sentry-trace\"));\n }\n\n const transaction = Sentry.startTransaction({\n name: `${reqMethod} ${reqUrl}`,\n op: \"http.server\",\n ...traceparentData,\n });\n\n ctx.__sentry_transaction = transaction;\n\n // We put the transaction on the scope so users can attach children to it\n Sentry.getCurrentHub().configureScope((scope) => {\n scope.setSpan(transaction);\n });\n\n ctx.res.on(\"finish\", () => {\n // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction closes\n setImmediate(() => {\n // If you're using koa router, set the matched route as transaction name\n if (ctx._matchedRoute) {\n const mountPath = ctx.mountPath || \"\";\n transaction.setName(`${reqMethod} ${mountPath}${ctx._matchedRoute}`);\n }\n\n transaction.setHttpStatus(ctx.status);\n transaction.finish();\n });\n });\n\n await next();\n};\n\nconst errorHandler: Koa.Middleware = async (ctx, next) => {\n try {\n await next();\n } catch (err) {\n Sentry.withScope((scope) => {\n scope.addEventProcessor((event) => {\n return Sentry.addRequestDataToEvent(event, ctx.request);\n });\n Sentry.captureException(err);\n });\n throw err;\n }\n};\n\nexport const registerSentryMiddlewares = (server: Koa): void => {\n debug(\"Registering Sentry middlewares\");\n\n server.use(errorHandler);\n server.use(requestHandler);\n server.use(tracingMiddleWare);\n};\n","import createDebug from \"debug\";\n\nexport const debug = createDebug(\"mud:store-indexer\");\nexport const error = createDebug(\"mud:store-indexer\");\n\n// Pipe debug output to stdout instead of stderr\ndebug.log = console.debug.bind(console);\n\n// Pipe error output to stderr\nerror.log = console.error.bind(console);\n","import { Middleware } from \"koa\";\nimport { Readable, Stream } from \"node:stream\";\nimport accepts from \"accepts\";\nimport { Zlib, createBrotliCompress, createDeflate, createGzip } from \"node:zlib\";\nimport { includes } from \"@latticexyz/common/utils\";\n\n// Loosely based on https://github.com/holic/koa-compress/blob/master/lib/index.js\n// with better handling of streams better with occasional flushing\n\nconst encodings = {\n br: createBrotliCompress,\n gzip: createGzip,\n deflate: createDeflate,\n} as const;\n\nconst encodingNames = Object.keys(encodings) as (keyof typeof encodings)[];\n\nfunction flushEvery<stream extends Zlib & Readable>(stream: stream, bytesThreshold: number): stream {\n let bytesSinceFlush = 0;\n stream.on(\"data\", (data) => {\n bytesSinceFlush += data.length;\n if (bytesSinceFlush > bytesThreshold) {\n bytesSinceFlush = 0;\n stream.flush();\n }\n });\n return stream;\n}\n\ntype CompressOptions = {\n flushThreshold?: number;\n};\n\nexport function compress({ flushThreshold = 1024 * 4 }: CompressOptions = {}): Middleware {\n return async function compressMiddleware(ctx, next) {\n ctx.vary(\"Accept-Encoding\");\n\n await next();\n\n const encoding = accepts(ctx.req).encoding(encodingNames);\n if (!includes(encodingNames, encoding)) return;\n\n const compressed = flushEvery(encodings[encoding](), flushThreshold);\n\n ctx.set(\"Content-Encoding\", encoding);\n ctx.body = ctx.body instanceof Stream ? ctx.body.pipe(compressed) : compressed.end(ctx.body);\n };\n}\n"],"mappings":"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","names":["Sentry","ProfilingIntegration","stripUrlQueryAndFragment","createDebug","debug","error","ProfilingIntegration","requestHandler","ctx","next","resolve","reject","scope","event","err","tracingMiddleWare","reqMethod","reqUrl","stripUrlQueryAndFragment","traceparentData","transaction","mountPath","errorHandler","registerSentryMiddlewares","server","debug","Stream","accepts","createBrotliCompress","createDeflate","createGzip","includes","encodings","encodingNames","flushEvery","stream","bytesThreshold","bytesSinceFlush","data","compress","flushThreshold","ctx","next","encoding","compressed"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latticexyz/store-indexer",
3
- "version": "2.0.0-next.14",
3
+ "version": "2.0.0-next.15",
4
4
  "description": "Minimal Typescript indexer for Store",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,35 +14,47 @@
14
14
  },
15
15
  "types": "src/index.ts",
16
16
  "bin": {
17
+ "postgres-decoded-indexer": "./dist/bin/postgres-decoded-indexer.js",
17
18
  "postgres-frontend": "./dist/bin/postgres-frontend.js",
18
19
  "postgres-indexer": "./dist/bin/postgres-indexer.js",
19
20
  "sqlite-indexer": "./dist/bin/sqlite-indexer.js"
20
21
  },
21
22
  "dependencies": {
22
- "@fastify/cors": "^8.3.0",
23
+ "@koa/cors": "^4.0.0",
24
+ "@koa/router": "^12.0.1",
25
+ "@sentry/node": "^7.86.0",
26
+ "@sentry/profiling-node": "^1.2.6",
27
+ "@sentry/utils": "^7.86.0",
23
28
  "@trpc/client": "10.34.0",
24
29
  "@trpc/server": "10.34.0",
25
- "@wagmi/chains": "^0.2.22",
30
+ "accepts": "^1.3.8",
26
31
  "better-sqlite3": "^8.6.0",
27
32
  "debug": "^4.3.4",
28
33
  "dotenv": "^16.0.3",
29
34
  "drizzle-orm": "^0.28.5",
30
- "fastify": "^4.21.0",
31
- "postgres": "^3.3.5",
35
+ "koa": "^2.14.2",
36
+ "koa-compose": "^4.1.0",
37
+ "postgres": "3.3.5",
32
38
  "rxjs": "7.5.5",
33
39
  "superjson": "^1.12.4",
40
+ "trpc-koa-adapter": "^1.1.3",
34
41
  "viem": "1.14.0",
35
42
  "zod": "^3.21.4",
36
- "@latticexyz/block-logs-stream": "2.0.0-next.14",
37
- "@latticexyz/common": "2.0.0-next.14",
38
- "@latticexyz/protocol-parser": "2.0.0-next.14",
39
- "@latticexyz/store": "2.0.0-next.14",
40
- "@latticexyz/store-sync": "2.0.0-next.14"
43
+ "@latticexyz/block-logs-stream": "2.0.0-next.15",
44
+ "@latticexyz/common": "2.0.0-next.15",
45
+ "@latticexyz/protocol-parser": "2.0.0-next.15",
46
+ "@latticexyz/store": "2.0.0-next.15",
47
+ "@latticexyz/store-sync": "2.0.0-next.15"
41
48
  },
42
49
  "devDependencies": {
50
+ "@types/accepts": "^1.3.7",
43
51
  "@types/better-sqlite3": "^7.6.4",
44
52
  "@types/cors": "^2.8.13",
45
53
  "@types/debug": "^4.1.7",
54
+ "@types/koa": "^2.13.12",
55
+ "@types/koa-compose": "^3.2.8",
56
+ "@types/koa__cors": "^4.0.3",
57
+ "@types/koa__router": "^12.0.4",
46
58
  "concurrently": "^8.2.2",
47
59
  "tsup": "^6.7.0",
48
60
  "tsx": "^3.12.6",
@@ -60,11 +72,14 @@
60
72
  "dev": "tsup --watch",
61
73
  "lint": "eslint .",
62
74
  "start:postgres": "concurrently -n indexer,frontend -c cyan,magenta 'tsx bin/postgres-indexer' 'tsx bin/postgres-frontend'",
63
- "start:postgres:local": "DEBUG=mud:store-sync:createStoreSync DATABASE_URL=postgres://127.0.0.1/postgres RPC_HTTP_URL=http://127.0.0.1:8545 pnpm start:postgres",
64
- "start:postgres:testnet": "DEBUG=mud:store-sync:createStoreSync DATABASE_URL=postgres://127.0.0.1/postgres RPC_HTTP_URL=https://follower.testnet-chain.linfra.xyz pnpm start:postgres",
75
+ "start:postgres-decoded": "tsx bin/postgres-decoded-indexer",
76
+ "start:postgres-decoded:local": "DATABASE_URL=postgres://127.0.0.1/postgres RPC_HTTP_URL=http://127.0.0.1:8545 pnpm start:postgres-decoded",
77
+ "start:postgres-decoded:testnet": "DATABASE_URL=postgres://127.0.0.1/postgres RPC_HTTP_URL=https://rpc.holesky.redstone.xyz pnpm start:postgres-decoded",
78
+ "start:postgres:local": "DATABASE_URL=postgres://127.0.0.1/postgres RPC_HTTP_URL=http://127.0.0.1:8545 pnpm start:postgres",
79
+ "start:postgres:testnet": "DATABASE_URL=postgres://127.0.0.1/postgres RPC_HTTP_URL=https://rpc.holesky.redstone.xyz pnpm start:postgres",
65
80
  "start:sqlite": "tsx bin/sqlite-indexer",
66
- "start:sqlite:local": "DEBUG=mud:store-sync:createStoreSync SQLITE_FILENAME=anvil.db RPC_HTTP_URL=http://127.0.0.1:8545 pnpm start:sqlite",
67
- "start:sqlite:testnet": "DEBUG=mud:store-sync:createStoreSync SQLITE_FILENAME=testnet.db RPC_HTTP_URL=https://follower.testnet-chain.linfra.xyz pnpm start:sqlite",
81
+ "start:sqlite:local": "SQLITE_FILENAME=anvil.db RPC_HTTP_URL=http://127.0.0.1:8545 pnpm start:sqlite",
82
+ "start:sqlite:testnet": "SQLITE_FILENAME=testnet.db RPC_HTTP_URL=https://rpc.holesky.redstone.xyz pnpm start:sqlite",
68
83
  "test": "tsc --noEmit --skipLibCheck",
69
84
  "test:ci": "pnpm run test"
70
85
  }
@@ -0,0 +1,48 @@
1
+ import { Middleware } from "koa";
2
+ import { Readable, Stream } from "node:stream";
3
+ import accepts from "accepts";
4
+ import { Zlib, createBrotliCompress, createDeflate, createGzip } from "node:zlib";
5
+ import { includes } from "@latticexyz/common/utils";
6
+
7
+ // Loosely based on https://github.com/holic/koa-compress/blob/master/lib/index.js
8
+ // with better handling of streams better with occasional flushing
9
+
10
+ const encodings = {
11
+ br: createBrotliCompress,
12
+ gzip: createGzip,
13
+ deflate: createDeflate,
14
+ } as const;
15
+
16
+ const encodingNames = Object.keys(encodings) as (keyof typeof encodings)[];
17
+
18
+ function flushEvery<stream extends Zlib & Readable>(stream: stream, bytesThreshold: number): stream {
19
+ let bytesSinceFlush = 0;
20
+ stream.on("data", (data) => {
21
+ bytesSinceFlush += data.length;
22
+ if (bytesSinceFlush > bytesThreshold) {
23
+ bytesSinceFlush = 0;
24
+ stream.flush();
25
+ }
26
+ });
27
+ return stream;
28
+ }
29
+
30
+ type CompressOptions = {
31
+ flushThreshold?: number;
32
+ };
33
+
34
+ export function compress({ flushThreshold = 1024 * 4 }: CompressOptions = {}): Middleware {
35
+ return async function compressMiddleware(ctx, next) {
36
+ ctx.vary("Accept-Encoding");
37
+
38
+ await next();
39
+
40
+ const encoding = accepts(ctx.req).encoding(encodingNames);
41
+ if (!includes(encodingNames, encoding)) return;
42
+
43
+ const compressed = flushEvery(encodings[encoding](), flushThreshold);
44
+
45
+ ctx.set("Content-Encoding", encoding);
46
+ ctx.body = ctx.body instanceof Stream ? ctx.body.pipe(compressed) : compressed.end(ctx.body);
47
+ };
48
+ }
package/src/debug.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  import createDebug from "debug";
2
2
 
3
3
  export const debug = createDebug("mud:store-indexer");
4
+ export const error = createDebug("mud:store-indexer");
5
+
6
+ // Pipe debug output to stdout instead of stderr
7
+ debug.log = console.debug.bind(console);
8
+
9
+ // Pipe error output to stderr
10
+ error.log = console.error.bind(console);
@@ -0,0 +1,58 @@
1
+ import { Sql } from "postgres";
2
+ import { Middleware } from "koa";
3
+ import Router from "@koa/router";
4
+ import compose from "koa-compose";
5
+ import { input } from "@latticexyz/store-sync/indexer-client";
6
+ import { storeTables } from "@latticexyz/store-sync";
7
+ import { queryLogs } from "./queryLogs";
8
+ import { recordToLog } from "./recordToLog";
9
+ import { debug, error } from "../debug";
10
+ import { createBenchmark } from "@latticexyz/common";
11
+ import { compress } from "../compress";
12
+
13
+ export function apiRoutes(database: Sql): Middleware {
14
+ const router = new Router();
15
+
16
+ router.get("/api/logs", compress(), async (ctx) => {
17
+ const benchmark = createBenchmark("postgres:logs");
18
+ let options: ReturnType<typeof input.parse>;
19
+
20
+ try {
21
+ options = input.parse(typeof ctx.query.input === "string" ? JSON.parse(ctx.query.input) : {});
22
+ } catch (e) {
23
+ ctx.status = 400;
24
+ ctx.body = JSON.stringify(e);
25
+ debug(e);
26
+ return;
27
+ }
28
+
29
+ try {
30
+ options.filters = options.filters.length > 0 ? [...options.filters, { tableId: storeTables.Tables.tableId }] : [];
31
+ const records = await queryLogs(database, options ?? {}).execute();
32
+ benchmark("query records");
33
+ const logs = records.map(recordToLog);
34
+ benchmark("map records to logs");
35
+
36
+ if (records.length === 0) {
37
+ ctx.status = 404;
38
+ ctx.body = "no logs found";
39
+ error(
40
+ `no logs found for chainId ${options.chainId}, address ${options.address}, filters ${JSON.stringify(
41
+ options.filters
42
+ )}`
43
+ );
44
+ return;
45
+ }
46
+
47
+ const blockNumber = records[0].chainBlockNumber;
48
+ ctx.body = JSON.stringify({ blockNumber, logs });
49
+ ctx.status = 200;
50
+ } catch (e) {
51
+ ctx.status = 500;
52
+ ctx.body = JSON.stringify(e);
53
+ error(e);
54
+ }
55
+ });
56
+
57
+ return compose([router.routes(), router.allowedMethods()]) as Middleware;
58
+ }
@@ -0,0 +1,21 @@
1
+ import { Hex } from "viem";
2
+
3
+ export type RecordData = {
4
+ address: Hex;
5
+ tableId: Hex;
6
+ keyBytes: Hex;
7
+ staticData: Hex | null;
8
+ encodedLengths: Hex | null;
9
+ dynamicData: Hex | null;
10
+ recordBlockNumber: string;
11
+ logIndex: number;
12
+ };
13
+
14
+ export type RecordMetadata = {
15
+ indexerVersion: string;
16
+ chainId: string;
17
+ chainBlockNumber: string;
18
+ totalRows: number;
19
+ };
20
+
21
+ export type Record = RecordData & RecordMetadata;
@@ -0,0 +1,56 @@
1
+ import { getAddress } from "viem";
2
+ import { PgDatabase } from "drizzle-orm/pg-core";
3
+ import { TableWithRecords, isTableRegistrationLog, logToTable, storeTables } from "@latticexyz/store-sync";
4
+ import { decodeKey, decodeValueArgs } from "@latticexyz/protocol-parser";
5
+ import { QueryAdapter } from "@latticexyz/store-sync/trpc-indexer";
6
+ import { debug } from "../../debug";
7
+ import { getLogs } from "./getLogs";
8
+ import { groupBy } from "@latticexyz/common/utils";
9
+
10
+ /**
11
+ * Creates a query adapter for the tRPC server/client to query data from Postgres.
12
+ *
13
+ * @param {PgDatabase<any>} database Postgres database object from Drizzle
14
+ * @returns {Promise<QueryAdapter>} A set of methods used by tRPC endpoints.
15
+ * @deprecated
16
+ */
17
+ export async function createQueryAdapter(database: PgDatabase<any>): Promise<QueryAdapter> {
18
+ const adapter: QueryAdapter = {
19
+ async getLogs(opts) {
20
+ return getLogs(database, opts);
21
+ },
22
+ async findAll(opts) {
23
+ const filters = opts.filters ?? [];
24
+ const { blockNumber, logs } = await getLogs(database, {
25
+ ...opts,
26
+ // make sure we're always retrieving `store.Tables` table, so we can decode table values
27
+ filters: filters.length > 0 ? [...filters, { tableId: storeTables.Tables.tableId }] : [],
28
+ });
29
+
30
+ const tables = logs.filter(isTableRegistrationLog).map(logToTable);
31
+
32
+ const logsByTable = groupBy(logs, (log) => `${getAddress(log.address)}:${log.args.tableId}`);
33
+
34
+ const tablesWithRecords: TableWithRecords[] = tables.map((table) => {
35
+ const tableLogs = logsByTable.get(`${getAddress(table.address)}:${table.tableId}`) ?? [];
36
+ const records = tableLogs.map((log) => ({
37
+ key: decodeKey(table.keySchema, log.args.keyTuple),
38
+ value: decodeValueArgs(table.valueSchema, log.args),
39
+ }));
40
+
41
+ return {
42
+ ...table,
43
+ records,
44
+ };
45
+ });
46
+
47
+ debug("findAll: decoded %d logs across %d tables", logs.length, tables.length);
48
+
49
+ return {
50
+ blockNumber,
51
+ tables: tablesWithRecords,
52
+ };
53
+ },
54
+ };
55
+ return adapter;
56
+ }
@@ -0,0 +1,81 @@
1
+ import { PgDatabase } from "drizzle-orm/pg-core";
2
+ import { Hex } from "viem";
3
+ import { StorageAdapterLog, SyncFilter } from "@latticexyz/store-sync";
4
+ import { tables } from "@latticexyz/store-sync/postgres";
5
+ import { and, asc, eq, or } from "drizzle-orm";
6
+ import { bigIntMax } from "@latticexyz/common/utils";
7
+ import { recordToLog } from "../recordToLog";
8
+ import { createBenchmark } from "@latticexyz/common";
9
+
10
+ /**
11
+ * @deprecated
12
+ */
13
+ export async function getLogs(
14
+ database: PgDatabase<any>,
15
+ {
16
+ chainId,
17
+ address,
18
+ filters = [],
19
+ }: {
20
+ readonly chainId: number;
21
+ readonly address?: Hex;
22
+ readonly filters?: readonly SyncFilter[];
23
+ }
24
+ ): Promise<{ blockNumber: bigint; logs: (StorageAdapterLog & { eventName: "Store_SetRecord" })[] }> {
25
+ const benchmark = createBenchmark("drizzleGetLogs");
26
+
27
+ const conditions = filters.length
28
+ ? filters.map((filter) =>
29
+ and(
30
+ address != null ? eq(tables.recordsTable.address, address) : undefined,
31
+ eq(tables.recordsTable.tableId, filter.tableId),
32
+ filter.key0 != null ? eq(tables.recordsTable.key0, filter.key0) : undefined,
33
+ filter.key1 != null ? eq(tables.recordsTable.key1, filter.key1) : undefined
34
+ )
35
+ )
36
+ : address != null
37
+ ? [eq(tables.recordsTable.address, address)]
38
+ : [];
39
+ benchmark("parse config");
40
+
41
+ // Query for the block number that the indexer (i.e. chain) is at, in case the
42
+ // indexer is further along in the chain than a given store/table's last updated
43
+ // block number. We'll then take the highest block number between the indexer's
44
+ // chain state and all the records in the query (in case the records updated
45
+ // between these queries). Using just the highest block number from the queries
46
+ // could potentially signal to the client an older-than-necessary block number,
47
+ // for stores/tables that haven't seen recent activity.
48
+ // TODO: move the block number query into the records query for atomicity so we don't have to merge them here
49
+ const chainState = await database
50
+ .select()
51
+ .from(tables.configTable)
52
+ .where(eq(tables.configTable.chainId, chainId))
53
+ .limit(1)
54
+ .execute()
55
+ // Get the first record in a way that returns a possible `undefined`
56
+ // TODO: move this to `.findFirst` after upgrading drizzle or `rows[0]` after enabling `noUncheckedIndexedAccess: true`
57
+ .then((rows) => rows.find(() => true));
58
+ const indexerBlockNumber = chainState?.blockNumber ?? 0n;
59
+ benchmark("query chainState");
60
+
61
+ const records = await database
62
+ .select()
63
+ .from(tables.recordsTable)
64
+ .where(or(...conditions))
65
+ .orderBy(
66
+ asc(tables.recordsTable.blockNumber)
67
+ // TODO: add logIndex (https://github.com/latticexyz/mud/issues/1979)
68
+ );
69
+ benchmark("query records");
70
+
71
+ const blockNumber = records.reduce((max, record) => bigIntMax(max, record.blockNumber ?? 0n), indexerBlockNumber);
72
+ benchmark("find block number");
73
+
74
+ const logs = records
75
+ // TODO: add this to the query, assuming we can optimize with an index
76
+ .filter((record) => !record.isDeleted)
77
+ .map(recordToLog);
78
+ benchmark("map records to logs");
79
+
80
+ return { blockNumber, logs };
81
+ }
@@ -0,0 +1,72 @@
1
+ import { isNotNull } from "@latticexyz/common/utils";
2
+ import { PendingQuery, Row, Sql } from "postgres";
3
+ import { hexToBytes } from "viem";
4
+ import { z } from "zod";
5
+ import { input } from "@latticexyz/store-sync/indexer-client";
6
+ import { transformSchemaName } from "@latticexyz/store-sync/postgres";
7
+ import { Record } from "./common";
8
+
9
+ const schemaName = transformSchemaName("mud");
10
+
11
+ function and(sql: Sql, conditions: PendingQuery<Row[]>[]): PendingQuery<Row[]> {
12
+ return sql`(${conditions.reduce((query, condition) => sql`${query} AND ${condition}`)})`;
13
+ }
14
+
15
+ function or(sql: Sql, conditions: PendingQuery<Row[]>[]): PendingQuery<Row[]> {
16
+ return sql`(${conditions.reduce((query, condition) => sql`${query} OR ${condition}`)})`;
17
+ }
18
+
19
+ export function queryLogs(sql: Sql, opts: z.infer<typeof input>): PendingQuery<Record[]> {
20
+ const conditions = opts.filters.length
21
+ ? opts.filters.map((filter) =>
22
+ and(
23
+ sql,
24
+ [
25
+ opts.address != null ? sql`address = ${hexToBytes(opts.address)}` : null,
26
+ sql`table_id = ${hexToBytes(filter.tableId)}`,
27
+ filter.key0 != null ? sql`key0 = ${hexToBytes(filter.key0)}` : null,
28
+ filter.key1 != null ? sql`key1 = ${hexToBytes(filter.key1)}` : null,
29
+ ].filter(isNotNull)
30
+ )
31
+ )
32
+ : opts.address != null
33
+ ? [sql`address = ${hexToBytes(opts.address)}`]
34
+ : [];
35
+
36
+ const where = sql`WHERE ${and(
37
+ sql,
38
+ [sql`is_deleted != true`, conditions.length ? or(sql, conditions) : null].filter(isNotNull)
39
+ )}`;
40
+
41
+ // TODO: implement bytea <> hex columns via custom types: https://github.com/porsager/postgres#custom-types
42
+ // TODO: sort by logIndex (https://github.com/latticexyz/mud/issues/1979)
43
+ return sql<Record[]>`
44
+ WITH
45
+ config AS (
46
+ SELECT
47
+ version AS "indexerVersion",
48
+ chain_id AS "chainId",
49
+ block_number AS "chainBlockNumber"
50
+ FROM ${sql(`${schemaName}.config`)}
51
+ LIMIT 1
52
+ ),
53
+ records AS (
54
+ SELECT
55
+ '0x' || encode(address, 'hex') AS address,
56
+ '0x' || encode(table_id, 'hex') AS "tableId",
57
+ '0x' || encode(key_bytes, 'hex') AS "keyBytes",
58
+ '0x' || encode(static_data, 'hex') AS "staticData",
59
+ '0x' || encode(encoded_lengths, 'hex') AS "encodedLengths",
60
+ '0x' || encode(dynamic_data, 'hex') AS "dynamicData",
61
+ block_number AS "recordBlockNumber",
62
+ log_index AS "logIndex"
63
+ FROM ${sql(`${schemaName}.records`)}
64
+ ${where}
65
+ ORDER BY block_number, log_index ASC
66
+ )
67
+ SELECT
68
+ (SELECT COUNT(*) FROM records) AS "totalRows",
69
+ *
70
+ FROM config, records
71
+ `;
72
+ }
@@ -0,0 +1,19 @@
1
+ import { StorageAdapterLog } from "@latticexyz/store-sync";
2
+ import { decodeDynamicField } from "@latticexyz/protocol-parser";
3
+ import { RecordData } from "./common";
4
+
5
+ export function recordToLog(
6
+ record: Omit<RecordData, "recordBlockNumber">
7
+ ): StorageAdapterLog & { eventName: "Store_SetRecord" } {
8
+ return {
9
+ address: record.address,
10
+ eventName: "Store_SetRecord",
11
+ args: {
12
+ tableId: record.tableId,
13
+ keyTuple: decodeDynamicField("bytes32[]", record.keyBytes),
14
+ staticData: record.staticData ?? "0x",
15
+ encodedLengths: record.encodedLengths ?? "0x",
16
+ dynamicData: record.dynamicData ?? "0x",
17
+ },
18
+ } as const;
19
+ }
package/src/sentry.ts ADDED
@@ -0,0 +1,105 @@
1
+ import * as Sentry from "@sentry/node";
2
+ import { ProfilingIntegration } from "@sentry/profiling-node";
3
+ import { stripUrlQueryAndFragment } from "@sentry/utils";
4
+ import { debug } from "./debug";
5
+ import Koa from "koa";
6
+
7
+ Sentry.init({
8
+ dsn: process.env.SENTRY_DSN,
9
+ integrations: [
10
+ // Automatically instrument Node.js libraries and frameworks
11
+ ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(),
12
+ new ProfilingIntegration(),
13
+ ],
14
+ // Performance Monitoring
15
+ tracesSampleRate: 1.0,
16
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
17
+ profilesSampleRate: 1.0,
18
+ });
19
+
20
+ const requestHandler: Koa.Middleware = (ctx, next) => {
21
+ return new Promise<void>((resolve, reject) => {
22
+ Sentry.runWithAsyncContext(async () => {
23
+ const hub = Sentry.getCurrentHub();
24
+ hub.configureScope((scope) =>
25
+ scope.addEventProcessor((event) =>
26
+ Sentry.addRequestDataToEvent(event, ctx.request, {
27
+ include: {
28
+ user: false,
29
+ },
30
+ })
31
+ )
32
+ );
33
+
34
+ try {
35
+ await next();
36
+ } catch (err) {
37
+ reject(err);
38
+ }
39
+ resolve();
40
+ });
41
+ });
42
+ };
43
+
44
+ // This tracing middleware creates a transaction per request
45
+ const tracingMiddleWare: Koa.Middleware = async (ctx, next) => {
46
+ const reqMethod = (ctx.method || "").toUpperCase();
47
+ const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url);
48
+
49
+ // Connect to trace of upstream app
50
+ let traceparentData;
51
+ if (ctx.request.get("sentry-trace")) {
52
+ traceparentData = Sentry.extractTraceparentData(ctx.request.get("sentry-trace"));
53
+ }
54
+
55
+ const transaction = Sentry.startTransaction({
56
+ name: `${reqMethod} ${reqUrl}`,
57
+ op: "http.server",
58
+ ...traceparentData,
59
+ });
60
+
61
+ ctx.__sentry_transaction = transaction;
62
+
63
+ // We put the transaction on the scope so users can attach children to it
64
+ Sentry.getCurrentHub().configureScope((scope) => {
65
+ scope.setSpan(transaction);
66
+ });
67
+
68
+ ctx.res.on("finish", () => {
69
+ // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction closes
70
+ setImmediate(() => {
71
+ // If you're using koa router, set the matched route as transaction name
72
+ if (ctx._matchedRoute) {
73
+ const mountPath = ctx.mountPath || "";
74
+ transaction.setName(`${reqMethod} ${mountPath}${ctx._matchedRoute}`);
75
+ }
76
+
77
+ transaction.setHttpStatus(ctx.status);
78
+ transaction.finish();
79
+ });
80
+ });
81
+
82
+ await next();
83
+ };
84
+
85
+ const errorHandler: Koa.Middleware = async (ctx, next) => {
86
+ try {
87
+ await next();
88
+ } catch (err) {
89
+ Sentry.withScope((scope) => {
90
+ scope.addEventProcessor((event) => {
91
+ return Sentry.addRequestDataToEvent(event, ctx.request);
92
+ });
93
+ Sentry.captureException(err);
94
+ });
95
+ throw err;
96
+ }
97
+ };
98
+
99
+ export const registerSentryMiddlewares = (server: Koa): void => {
100
+ debug("Registering Sentry middlewares");
101
+
102
+ server.use(errorHandler);
103
+ server.use(requestHandler);
104
+ server.use(tracingMiddleWare);
105
+ };
@@ -0,0 +1,47 @@
1
+ import { Middleware } from "koa";
2
+ import Router from "@koa/router";
3
+ import compose from "koa-compose";
4
+ import { input } from "@latticexyz/store-sync/indexer-client";
5
+ import { storeTables, tablesWithRecordsToLogs } from "@latticexyz/store-sync";
6
+ import { debug } from "../debug";
7
+ import { createBenchmark } from "@latticexyz/common";
8
+ import { compress } from "../compress";
9
+ import { getTablesWithRecords } from "./getTablesWithRecords";
10
+ import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
11
+
12
+ export function apiRoutes(database: BaseSQLiteDatabase<"sync", any>): Middleware {
13
+ const router = new Router();
14
+
15
+ router.get("/api/logs", compress(), async (ctx) => {
16
+ const benchmark = createBenchmark("sqlite:logs");
17
+
18
+ let options: ReturnType<typeof input.parse>;
19
+
20
+ try {
21
+ options = input.parse(typeof ctx.query.input === "string" ? JSON.parse(ctx.query.input) : {});
22
+ } catch (error) {
23
+ ctx.status = 400;
24
+ ctx.body = JSON.stringify(error);
25
+ debug(error);
26
+ return;
27
+ }
28
+
29
+ try {
30
+ options.filters = options.filters.length > 0 ? [...options.filters, { tableId: storeTables.Tables.tableId }] : [];
31
+ benchmark("parse config");
32
+ const { blockNumber, tables } = getTablesWithRecords(database, options);
33
+ benchmark("query tables with records");
34
+ const logs = tablesWithRecordsToLogs(tables);
35
+ benchmark("convert records to logs");
36
+
37
+ ctx.body = JSON.stringify({ blockNumber: blockNumber?.toString() ?? "-1", logs });
38
+ ctx.status = 200;
39
+ } catch (error) {
40
+ ctx.status = 500;
41
+ ctx.body = JSON.stringify(error);
42
+ debug(error);
43
+ }
44
+ });
45
+
46
+ return compose([router.routes(), router.allowedMethods()]) as Middleware;
47
+ }
@@ -1,10 +1,7 @@
1
- import { eq } from "drizzle-orm";
2
1
  import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
3
- import { buildTable, chainState, getTables } from "@latticexyz/store-sync/sqlite";
4
2
  import { QueryAdapter } from "@latticexyz/store-sync/trpc-indexer";
5
- import { debug } from "../debug";
6
- import { getAddress } from "viem";
7
- import { decodeDynamicField } from "@latticexyz/protocol-parser";
3
+ import { getTablesWithRecords } from "./getTablesWithRecords";
4
+ import { tablesWithRecordsToLogs } from "@latticexyz/store-sync";
8
5
 
9
6
  /**
10
7
  * Creates a storage adapter for the tRPC server/client to query data from SQLite.
@@ -14,48 +11,13 @@ import { decodeDynamicField } from "@latticexyz/protocol-parser";
14
11
  */
15
12
  export async function createQueryAdapter(database: BaseSQLiteDatabase<"sync", any>): Promise<QueryAdapter> {
16
13
  const adapter: QueryAdapter = {
17
- async findAll({ chainId, address, filters = [] }) {
18
- // If _any_ filter has a table ID, this will filter down all data to just those tables. Which mean we can't yet mix table filters with key-only filters.
19
- // TODO: improve this so we can express this in the query (need to be able to query data across tables more easily)
20
- const tableIds = Array.from(new Set(filters.map((filter) => filter.tableId)));
21
- const tables = getTables(database)
22
- .filter((table) => address == null || getAddress(address) === getAddress(table.address))
23
- .filter((table) => !tableIds.length || tableIds.includes(table.tableId));
24
-
25
- const tablesWithRecords = tables.map((table) => {
26
- const sqliteTable = buildTable(table);
27
- const records = database.select().from(sqliteTable).where(eq(sqliteTable.__isDeleted, false)).all();
28
- const filteredRecords = !filters.length
29
- ? records
30
- : records.filter((record) => {
31
- const keyTuple = decodeDynamicField("bytes32[]", record.__key);
32
- return filters.some(
33
- (filter) =>
34
- filter.tableId === table.tableId &&
35
- (filter.key0 == null || filter.key0 === keyTuple[0]) &&
36
- (filter.key1 == null || filter.key1 === keyTuple[1])
37
- );
38
- });
39
- return {
40
- ...table,
41
- records: filteredRecords.map((record) => ({
42
- key: Object.fromEntries(Object.entries(table.keySchema).map(([name]) => [name, record[name]])),
43
- value: Object.fromEntries(Object.entries(table.valueSchema).map(([name]) => [name, record[name]])),
44
- })),
45
- };
46
- });
47
-
48
- const metadata = database.select().from(chainState).where(eq(chainState.chainId, chainId)).all();
49
- const { lastUpdatedBlockNumber } = metadata[0] ?? {};
50
-
51
- const result = {
52
- blockNumber: lastUpdatedBlockNumber ?? null,
53
- tables: tablesWithRecords,
54
- };
55
-
56
- debug("findAll", chainId, address, result);
57
-
58
- return result;
14
+ async getLogs(opts) {
15
+ const { blockNumber, tables } = getTablesWithRecords(database, opts);
16
+ const logs = tablesWithRecordsToLogs(tables);
17
+ return { blockNumber: blockNumber ?? 0n, logs };
18
+ },
19
+ async findAll(opts) {
20
+ return getTablesWithRecords(database, opts);
59
21
  },
60
22
  };
61
23
  return adapter;
@@ -0,0 +1,75 @@
1
+ import { asc, eq } from "drizzle-orm";
2
+ import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
3
+ import { buildTable, chainState, getTables } from "@latticexyz/store-sync/sqlite";
4
+ import { Hex, getAddress } from "viem";
5
+ import { decodeDynamicField } from "@latticexyz/protocol-parser";
6
+ import { SyncFilter, TableWithRecords } from "@latticexyz/store-sync";
7
+
8
+ // TODO: refactor sqlite and replace this with getLogs to match postgres (https://github.com/latticexyz/mud/issues/1970)
9
+
10
+ /**
11
+ * @deprecated
12
+ * */
13
+ export function getTablesWithRecords(
14
+ database: BaseSQLiteDatabase<"sync", any>,
15
+ {
16
+ chainId,
17
+ address,
18
+ filters = [],
19
+ }: {
20
+ readonly chainId: number;
21
+ readonly address?: Hex;
22
+ readonly filters?: readonly SyncFilter[];
23
+ }
24
+ ): { blockNumber: bigint | null; tables: readonly TableWithRecords[] } {
25
+ const metadata = database
26
+ .select()
27
+ .from(chainState)
28
+ .where(eq(chainState.chainId, chainId))
29
+ .limit(1)
30
+ .all()
31
+ .find(() => true);
32
+
33
+ // If _any_ filter has a table ID, this will filter down all data to just those tables. Which mean we can't yet mix table filters with key-only filters.
34
+ // TODO: improve this so we can express this in the query (need to be able to query data across tables more easily)
35
+ const tableIds = Array.from(new Set(filters.map((filter) => filter.tableId)));
36
+ const tables = getTables(database)
37
+ .filter((table) => address == null || getAddress(address) === getAddress(table.address))
38
+ .filter((table) => !tableIds.length || tableIds.includes(table.tableId));
39
+
40
+ const tablesWithRecords = tables.map((table) => {
41
+ const sqliteTable = buildTable(table);
42
+ const records = database
43
+ .select()
44
+ .from(sqliteTable)
45
+ .where(eq(sqliteTable.__isDeleted, false))
46
+ .orderBy(
47
+ asc(sqliteTable.__lastUpdatedBlockNumber)
48
+ // TODO: add logIndex (https://github.com/latticexyz/mud/issues/1979)
49
+ )
50
+ .all();
51
+ const filteredRecords = !filters.length
52
+ ? records
53
+ : records.filter((record) => {
54
+ const keyTuple = decodeDynamicField("bytes32[]", record.__key);
55
+ return filters.some(
56
+ (filter) =>
57
+ filter.tableId === table.tableId &&
58
+ (filter.key0 == null || filter.key0 === keyTuple[0]) &&
59
+ (filter.key1 == null || filter.key1 === keyTuple[1])
60
+ );
61
+ });
62
+ return {
63
+ ...table,
64
+ records: filteredRecords.map((record) => ({
65
+ key: Object.fromEntries(Object.entries(table.keySchema).map(([name]) => [name, record[name]])),
66
+ value: Object.fromEntries(Object.entries(table.valueSchema).map(([name]) => [name, record[name]])),
67
+ })),
68
+ };
69
+ });
70
+
71
+ return {
72
+ blockNumber: metadata?.lastUpdatedBlockNumber ?? null,
73
+ tables: tablesWithRecords,
74
+ };
75
+ }
@@ -1,2 +0,0 @@
1
- import e from"debug";var o=e("mud:store-indexer");export{o as a};
2
- //# sourceMappingURL=chunk-LOFAZSVJ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/debug.ts"],"sourcesContent":["import createDebug from \"debug\";\n\nexport const debug = createDebug(\"mud:store-indexer\");\n"],"mappings":"AAAA,OAAOA,MAAiB,QAEjB,IAAMC,EAAQD,EAAY,mBAAmB","names":["createDebug","debug"]}
@@ -1,69 +0,0 @@
1
- import { eq } from "drizzle-orm";
2
- import { PgDatabase } from "drizzle-orm/pg-core";
3
- import { buildTable, buildInternalTables, getTables } from "@latticexyz/store-sync/postgres";
4
- import { QueryAdapter } from "@latticexyz/store-sync/trpc-indexer";
5
- import { debug } from "../debug";
6
- import { getAddress } from "viem";
7
- import { decodeDynamicField } from "@latticexyz/protocol-parser";
8
-
9
- /**
10
- * Creates a query adapter for the tRPC server/client to query data from Postgres.
11
- *
12
- * @param {PgDatabase<any>} database Postgres database object from Drizzle
13
- * @returns {Promise<QueryAdapter>} A set of methods used by tRPC endpoints.
14
- */
15
- export async function createQueryAdapter(database: PgDatabase<any>): Promise<QueryAdapter> {
16
- const adapter: QueryAdapter = {
17
- async findAll({ chainId, address, filters = [] }) {
18
- // If _any_ filter has a table ID, this will filter down all data to just those tables. Which mean we can't yet mix table filters with key-only filters.
19
- // TODO: improve this so we can express this in the query (need to be able to query data across tables more easily)
20
- const tableIds = Array.from(new Set(filters.map((filter) => filter.tableId)));
21
- const tables = (await getTables(database))
22
- .filter((table) => address == null || getAddress(address) === getAddress(table.address))
23
- .filter((table) => !tableIds.length || tableIds.includes(table.tableId));
24
-
25
- const tablesWithRecords = await Promise.all(
26
- tables.map(async (table) => {
27
- const sqliteTable = buildTable(table);
28
- const records = await database.select().from(sqliteTable).where(eq(sqliteTable.__isDeleted, false)).execute();
29
- const filteredRecords = !filters.length
30
- ? records
31
- : records.filter((record) => {
32
- const keyTuple = decodeDynamicField("bytes32[]", record.__key);
33
- return filters.some(
34
- (filter) =>
35
- filter.tableId === table.tableId &&
36
- (filter.key0 == null || filter.key0 === keyTuple[0]) &&
37
- (filter.key1 == null || filter.key1 === keyTuple[1])
38
- );
39
- });
40
- return {
41
- ...table,
42
- records: filteredRecords.map((record) => ({
43
- key: Object.fromEntries(Object.entries(table.keySchema).map(([name]) => [name, record[name]])),
44
- value: Object.fromEntries(Object.entries(table.valueSchema).map(([name]) => [name, record[name]])),
45
- })),
46
- };
47
- })
48
- );
49
-
50
- const internalTables = buildInternalTables();
51
- const metadata = await database
52
- .select()
53
- .from(internalTables.chain)
54
- .where(eq(internalTables.chain.chainId, chainId))
55
- .execute();
56
- const { lastUpdatedBlockNumber } = metadata[0] ?? {};
57
-
58
- const result = {
59
- blockNumber: lastUpdatedBlockNumber ?? null,
60
- tables: tablesWithRecords,
61
- };
62
-
63
- debug("findAll", chainId, address, result);
64
-
65
- return result;
66
- },
67
- };
68
- return adapter;
69
- }