ponder 0.14.13 → 0.15.0

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.
Files changed (237) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/esm/bin/commands/createViews.js +28 -11
  3. package/dist/esm/bin/commands/createViews.js.map +1 -1
  4. package/dist/esm/bin/commands/dev.js +42 -22
  5. package/dist/esm/bin/commands/dev.js.map +1 -1
  6. package/dist/esm/bin/commands/prune.js +3 -0
  7. package/dist/esm/bin/commands/prune.js.map +1 -1
  8. package/dist/esm/bin/commands/serve.js +4 -1
  9. package/dist/esm/bin/commands/serve.js.map +1 -1
  10. package/dist/esm/bin/commands/start.js +18 -6
  11. package/dist/esm/bin/commands/start.js.map +1 -1
  12. package/dist/esm/bin/isolatedController.js +200 -0
  13. package/dist/esm/bin/isolatedController.js.map +1 -0
  14. package/dist/esm/bin/isolatedWorker.js +146 -0
  15. package/dist/esm/bin/isolatedWorker.js.map +1 -0
  16. package/dist/esm/build/config.js +322 -402
  17. package/dist/esm/build/config.js.map +1 -1
  18. package/dist/esm/build/index.js +8 -11
  19. package/dist/esm/build/index.js.map +1 -1
  20. package/dist/esm/build/pre.js +1 -4
  21. package/dist/esm/build/pre.js.map +1 -1
  22. package/dist/esm/build/schema.js +25 -3
  23. package/dist/esm/build/schema.js.map +1 -1
  24. package/dist/esm/client/index.js +306 -42
  25. package/dist/esm/client/index.js.map +1 -1
  26. package/dist/esm/database/actions.js +264 -104
  27. package/dist/esm/database/actions.js.map +1 -1
  28. package/dist/esm/database/index.js +39 -33
  29. package/dist/esm/database/index.js.map +1 -1
  30. package/dist/esm/database/queryBuilder.js +1 -0
  31. package/dist/esm/database/queryBuilder.js.map +1 -1
  32. package/dist/esm/drizzle/index.js +11 -7
  33. package/dist/esm/drizzle/index.js.map +1 -1
  34. package/dist/esm/drizzle/onchain.js +18 -0
  35. package/dist/esm/drizzle/onchain.js.map +1 -1
  36. package/dist/esm/indexing/client.js +32 -25
  37. package/dist/esm/indexing/client.js.map +1 -1
  38. package/dist/esm/indexing/index.js +110 -178
  39. package/dist/esm/indexing/index.js.map +1 -1
  40. package/dist/esm/indexing/profile.js +1 -1
  41. package/dist/esm/indexing/profile.js.map +1 -1
  42. package/dist/esm/indexing-store/cache.js +196 -274
  43. package/dist/esm/indexing-store/cache.js.map +1 -1
  44. package/dist/esm/indexing-store/historical.js +17 -13
  45. package/dist/esm/indexing-store/historical.js.map +1 -1
  46. package/dist/esm/indexing-store/index.js +10 -1
  47. package/dist/esm/indexing-store/index.js.map +1 -1
  48. package/dist/esm/indexing-store/profile.js +3 -3
  49. package/dist/esm/indexing-store/profile.js.map +1 -1
  50. package/dist/esm/indexing-store/realtime.js +27 -2
  51. package/dist/esm/indexing-store/realtime.js.map +1 -1
  52. package/dist/esm/internal/errors.js +28 -0
  53. package/dist/esm/internal/errors.js.map +1 -1
  54. package/dist/esm/internal/metrics.js +279 -82
  55. package/dist/esm/internal/metrics.js.map +1 -1
  56. package/dist/esm/internal/options.js +1 -0
  57. package/dist/esm/internal/options.js.map +1 -1
  58. package/dist/esm/internal/telemetry.js +1 -1
  59. package/dist/esm/internal/telemetry.js.map +1 -1
  60. package/dist/esm/rpc/http.js +130 -0
  61. package/dist/esm/rpc/http.js.map +1 -0
  62. package/dist/esm/rpc/index.js +38 -7
  63. package/dist/esm/rpc/index.js.map +1 -1
  64. package/dist/esm/runtime/events.js +179 -212
  65. package/dist/esm/runtime/events.js.map +1 -1
  66. package/dist/esm/runtime/filter.js +71 -0
  67. package/dist/esm/runtime/filter.js.map +1 -1
  68. package/dist/esm/runtime/fragments.js +78 -73
  69. package/dist/esm/runtime/fragments.js.map +1 -1
  70. package/dist/esm/runtime/historical.js +306 -130
  71. package/dist/esm/runtime/historical.js.map +1 -1
  72. package/dist/esm/runtime/index.js +183 -58
  73. package/dist/esm/runtime/index.js.map +1 -1
  74. package/dist/esm/runtime/isolated.js +462 -0
  75. package/dist/esm/runtime/isolated.js.map +1 -0
  76. package/dist/esm/runtime/multichain.js +80 -73
  77. package/dist/esm/runtime/multichain.js.map +1 -1
  78. package/dist/esm/runtime/omnichain.js +82 -75
  79. package/dist/esm/runtime/omnichain.js.map +1 -1
  80. package/dist/esm/runtime/realtime.js +198 -66
  81. package/dist/esm/runtime/realtime.js.map +1 -1
  82. package/dist/esm/sync-historical/index.js +416 -457
  83. package/dist/esm/sync-historical/index.js.map +1 -1
  84. package/dist/esm/sync-realtime/bloom.js +3 -3
  85. package/dist/esm/sync-realtime/bloom.js.map +1 -1
  86. package/dist/esm/sync-realtime/index.js +27 -46
  87. package/dist/esm/sync-realtime/index.js.map +1 -1
  88. package/dist/esm/sync-store/index.js +112 -63
  89. package/dist/esm/sync-store/index.js.map +1 -1
  90. package/dist/esm/utils/abi.js +20 -32
  91. package/dist/esm/utils/abi.js.map +1 -1
  92. package/dist/esm/utils/chunk.js +8 -0
  93. package/dist/esm/utils/chunk.js.map +1 -0
  94. package/dist/esm/utils/promiseAllSettledWithThrow.js +19 -0
  95. package/dist/esm/utils/promiseAllSettledWithThrow.js.map +1 -0
  96. package/dist/esm/{client/parse.js → utils/sql-parse.js} +94 -80
  97. package/dist/esm/utils/sql-parse.js.map +1 -0
  98. package/dist/types/bin/commands/createViews.d.ts.map +1 -1
  99. package/dist/types/bin/commands/dev.d.ts.map +1 -1
  100. package/dist/types/bin/commands/prune.d.ts.map +1 -1
  101. package/dist/types/bin/commands/serve.d.ts.map +1 -1
  102. package/dist/types/bin/commands/start.d.ts.map +1 -1
  103. package/dist/types/bin/isolatedController.d.ts +13 -0
  104. package/dist/types/bin/isolatedController.d.ts.map +1 -0
  105. package/dist/types/bin/isolatedWorker.d.ts +9 -0
  106. package/dist/types/bin/isolatedWorker.d.ts.map +1 -0
  107. package/dist/types/build/config.d.ts +29 -11
  108. package/dist/types/build/config.d.ts.map +1 -1
  109. package/dist/types/build/index.d.ts +3 -2
  110. package/dist/types/build/index.d.ts.map +1 -1
  111. package/dist/types/build/pre.d.ts +1 -1
  112. package/dist/types/build/pre.d.ts.map +1 -1
  113. package/dist/types/build/schema.d.ts +5 -3
  114. package/dist/types/build/schema.d.ts.map +1 -1
  115. package/dist/types/client/index.d.ts +1 -1
  116. package/dist/types/client/index.d.ts.map +1 -1
  117. package/dist/types/config/index.d.ts +3 -3
  118. package/dist/types/config/index.d.ts.map +1 -1
  119. package/dist/types/database/actions.d.ts +53 -7
  120. package/dist/types/database/actions.d.ts.map +1 -1
  121. package/dist/types/database/index.d.ts +21 -21
  122. package/dist/types/database/index.d.ts.map +1 -1
  123. package/dist/types/database/queryBuilder.d.ts.map +1 -1
  124. package/dist/types/drizzle/index.d.ts +4 -5
  125. package/dist/types/drizzle/index.d.ts.map +1 -1
  126. package/dist/types/drizzle/onchain.d.ts +6 -0
  127. package/dist/types/drizzle/onchain.d.ts.map +1 -1
  128. package/dist/types/indexing/client.d.ts.map +1 -1
  129. package/dist/types/indexing/index.d.ts +2 -5
  130. package/dist/types/indexing/index.d.ts.map +1 -1
  131. package/dist/types/indexing-store/cache.d.ts +3 -2
  132. package/dist/types/indexing-store/cache.d.ts.map +1 -1
  133. package/dist/types/indexing-store/historical.d.ts +2 -1
  134. package/dist/types/indexing-store/historical.d.ts.map +1 -1
  135. package/dist/types/indexing-store/index.d.ts +1 -0
  136. package/dist/types/indexing-store/index.d.ts.map +1 -1
  137. package/dist/types/indexing-store/realtime.d.ts +2 -1
  138. package/dist/types/indexing-store/realtime.d.ts.map +1 -1
  139. package/dist/types/internal/errors.d.ts +5 -0
  140. package/dist/types/internal/errors.d.ts.map +1 -1
  141. package/dist/types/internal/metrics.d.ts +21 -0
  142. package/dist/types/internal/metrics.d.ts.map +1 -1
  143. package/dist/types/internal/options.d.ts +2 -0
  144. package/dist/types/internal/options.d.ts.map +1 -1
  145. package/dist/types/internal/types.d.ts +66 -58
  146. package/dist/types/internal/types.d.ts.map +1 -1
  147. package/dist/types/rpc/http.d.ts +17 -0
  148. package/dist/types/rpc/http.d.ts.map +1 -0
  149. package/dist/types/rpc/index.d.ts.map +1 -1
  150. package/dist/types/runtime/events.d.ts +4 -4
  151. package/dist/types/runtime/events.d.ts.map +1 -1
  152. package/dist/types/runtime/filter.d.ts +5 -1
  153. package/dist/types/runtime/filter.d.ts.map +1 -1
  154. package/dist/types/runtime/fragments.d.ts +4 -3
  155. package/dist/types/runtime/fragments.d.ts.map +1 -1
  156. package/dist/types/runtime/historical.d.ts +29 -13
  157. package/dist/types/runtime/historical.d.ts.map +1 -1
  158. package/dist/types/runtime/index.d.ts +49 -6
  159. package/dist/types/runtime/index.d.ts.map +1 -1
  160. package/dist/types/runtime/init.d.ts +5 -5
  161. package/dist/types/runtime/init.d.ts.map +1 -1
  162. package/dist/types/runtime/isolated.d.ts +14 -0
  163. package/dist/types/runtime/isolated.d.ts.map +1 -0
  164. package/dist/types/runtime/multichain.d.ts.map +1 -1
  165. package/dist/types/runtime/omnichain.d.ts.map +1 -1
  166. package/dist/types/runtime/realtime.d.ts +21 -10
  167. package/dist/types/runtime/realtime.d.ts.map +1 -1
  168. package/dist/types/sync-historical/index.d.ts +18 -8
  169. package/dist/types/sync-historical/index.d.ts.map +1 -1
  170. package/dist/types/sync-realtime/bloom.d.ts.map +1 -1
  171. package/dist/types/sync-realtime/index.d.ts +2 -2
  172. package/dist/types/sync-realtime/index.d.ts.map +1 -1
  173. package/dist/types/sync-store/index.d.ts +9 -9
  174. package/dist/types/sync-store/index.d.ts.map +1 -1
  175. package/dist/types/utils/abi.d.ts +3 -34
  176. package/dist/types/utils/abi.d.ts.map +1 -1
  177. package/dist/types/utils/chunk.d.ts +2 -0
  178. package/dist/types/utils/chunk.d.ts.map +1 -0
  179. package/dist/types/utils/promiseAllSettledWithThrow.d.ts +8 -0
  180. package/dist/types/utils/promiseAllSettledWithThrow.d.ts.map +1 -0
  181. package/dist/types/utils/sql-parse.d.ts +21 -0
  182. package/dist/types/utils/sql-parse.d.ts.map +1 -0
  183. package/package.json +2 -2
  184. package/src/bin/commands/createViews.ts +35 -15
  185. package/src/bin/commands/dev.ts +43 -21
  186. package/src/bin/commands/prune.ts +6 -0
  187. package/src/bin/commands/serve.ts +4 -1
  188. package/src/bin/commands/start.ts +20 -5
  189. package/src/bin/isolatedController.ts +300 -0
  190. package/src/bin/isolatedWorker.ts +192 -0
  191. package/src/build/config.ts +570 -632
  192. package/src/build/index.ts +14 -14
  193. package/src/build/pre.ts +1 -4
  194. package/src/build/schema.ts +49 -4
  195. package/src/client/index.ts +386 -48
  196. package/src/config/index.ts +3 -3
  197. package/src/database/actions.ts +469 -120
  198. package/src/database/index.ts +85 -58
  199. package/src/database/queryBuilder.ts +1 -0
  200. package/src/drizzle/index.ts +15 -7
  201. package/src/drizzle/onchain.ts +19 -0
  202. package/src/indexing/client.ts +38 -25
  203. package/src/indexing/index.ts +137 -230
  204. package/src/indexing/profile.ts +1 -1
  205. package/src/indexing-store/cache.ts +285 -414
  206. package/src/indexing-store/historical.ts +20 -10
  207. package/src/indexing-store/index.ts +16 -0
  208. package/src/indexing-store/profile.ts +3 -3
  209. package/src/indexing-store/realtime.ts +28 -0
  210. package/src/internal/errors.ts +26 -0
  211. package/src/internal/metrics.ts +341 -111
  212. package/src/internal/options.ts +4 -0
  213. package/src/internal/telemetry.ts +1 -1
  214. package/src/internal/types.ts +70 -87
  215. package/src/rpc/http.ts +164 -0
  216. package/src/rpc/index.ts +39 -7
  217. package/src/runtime/events.ts +195 -240
  218. package/src/runtime/filter.ts +85 -1
  219. package/src/runtime/fragments.ts +109 -113
  220. package/src/runtime/historical.ts +467 -189
  221. package/src/runtime/index.ts +337 -69
  222. package/src/runtime/init.ts +5 -5
  223. package/src/runtime/isolated.ts +768 -0
  224. package/src/runtime/multichain.ts +137 -102
  225. package/src/runtime/omnichain.ts +138 -106
  226. package/src/runtime/realtime.ts +322 -123
  227. package/src/sync-historical/index.ts +556 -692
  228. package/src/sync-realtime/bloom.ts +7 -3
  229. package/src/sync-realtime/index.ts +31 -46
  230. package/src/sync-store/index.ts +189 -95
  231. package/src/utils/abi.ts +33 -90
  232. package/src/utils/chunk.ts +7 -0
  233. package/src/utils/promiseAllSettledWithThrow.ts +27 -0
  234. package/src/{client/parse.ts → utils/sql-parse.ts} +100 -90
  235. package/dist/esm/client/parse.js.map +0 -1
  236. package/dist/types/client/parse.d.ts +0 -14
  237. package/dist/types/client/parse.d.ts.map +0 -1
@@ -1,13 +1,56 @@
1
+ import type { PonderApp5 } from "@/database/index.js";
2
+ import { getLiveQueryChannelName } from "@/drizzle/onchain.js";
1
3
  import type { Schema } from "@/internal/types.js";
2
4
  import type { ReadonlyDrizzle } from "@/types/db.js";
3
- import { promiseWithResolvers } from "@/utils/promiseWithResolvers.js";
4
- import type { QueryWithTypings } from "drizzle-orm";
5
- import type { PgSession } from "drizzle-orm/pg-core";
5
+ import {
6
+ type PromiseWithResolvers,
7
+ promiseWithResolvers,
8
+ } from "@/utils/promiseWithResolvers.js";
9
+ import {
10
+ getSQLQueryRelations,
11
+ validateAllowableSQLQuery,
12
+ } from "@/utils/sql-parse.js";
13
+ import {
14
+ type QueryWithTypings,
15
+ getTableName,
16
+ getViewName,
17
+ isTable,
18
+ isView,
19
+ } from "drizzle-orm";
20
+ import {
21
+ type PgDialect,
22
+ type PgSession,
23
+ type PgView,
24
+ getViewConfig,
25
+ pgSchema,
26
+ pgTable,
27
+ } from "drizzle-orm/pg-core";
6
28
  import { createMiddleware } from "hono/factory";
7
29
  import { streamSSE } from "hono/streaming";
8
30
  import type * as pg from "pg";
9
31
  import superjson from "superjson";
10
- import { validateQuery } from "./parse.js";
32
+
33
+ type QueryString = string;
34
+ type QueryResult = unknown;
35
+
36
+ const MAX_LIVE_QUERIES = 1000;
37
+
38
+ /**
39
+ * @dev This is copied to avoid bundling another dependency.
40
+ */
41
+ const getPonderMetaTable = (schema?: string) => {
42
+ if (schema === undefined || schema === "public") {
43
+ return pgTable("_ponder_meta", (t) => ({
44
+ key: t.text().primaryKey().$type<"app">(),
45
+ value: t.jsonb().$type<PonderApp5>().notNull(),
46
+ }));
47
+ }
48
+
49
+ return pgSchema(schema).table("_ponder_meta", (t) => ({
50
+ key: t.text().primaryKey().$type<"app">(),
51
+ value: t.jsonb().$type<PonderApp5>().notNull(),
52
+ }));
53
+ };
11
54
 
12
55
  /**
13
56
  * Middleware for `@ponder/client`.
@@ -31,6 +74,7 @@ import { validateQuery } from "./parse.js";
31
74
  */
32
75
  export const client = ({
33
76
  db,
77
+ schema,
34
78
  }: { db: ReadonlyDrizzle<Schema>; schema: Schema }) => {
35
79
  if (
36
80
  globalThis.PONDER_COMMON === undefined ||
@@ -42,23 +86,147 @@ export const client = ({
42
86
  );
43
87
  }
44
88
 
89
+ const tables = Object.values(schema).filter(isTable);
90
+ const views = Object.values(schema).filter(isView);
91
+ const tableNames = new Set(tables.map(getTableName));
92
+ const viewNames = new Set(views.map(getViewName));
93
+
94
+ // Note: Add system tables to the live query registry.
95
+ tableNames.add("_ponder_checkpoint");
96
+
45
97
  // @ts-ignore
46
98
  const session: PgSession = db._.session;
99
+ // @ts-ignore
100
+ const dialect: PgDialect = session.dialect;
47
101
  const driver = globalThis.PONDER_DATABASE.driver;
48
- let statusResolver = promiseWithResolvers<void>();
49
102
 
50
- const channel = `${globalThis.PONDER_NAMESPACE_BUILD.schema}_status_channel`;
103
+ const perTableResolver = new Map<string, PromiseWithResolvers<void>>();
104
+ const perViewTables = new Map<string, Set<string>>();
51
105
 
52
- const cache = new Map<string, WeakRef<Promise<unknown>>>();
106
+ /** `true` if the app is indexing live blocks. */
107
+ let liveQueryCount = 0;
108
+ let isReady = false;
109
+
110
+ (async () => {
111
+ while (globalThis.PONDER_COMMON.apiShutdown.isKilled === false) {
112
+ try {
113
+ isReady = await globalThis.PONDER_DATABASE.readonlyQB.wrap(
114
+ { label: "select_ready" },
115
+ (db) =>
116
+ db
117
+ .select()
118
+ .from(getPonderMetaTable())
119
+ .then((result) => result[0]!.value.is_ready === 1),
120
+ );
121
+ } catch {}
122
+ if (isReady) return;
123
+ await new Promise((resolve) => setTimeout(resolve, 1000));
124
+ }
125
+ })();
126
+
127
+ const cache = new Map<QueryString, WeakRef<Promise<unknown>>>();
128
+ const perQueryReferences = new Map<QueryString, Set<string>>();
129
+
130
+ const registry = new FinalizationRegistry<QueryString>((queryString) => {
131
+ // Note: When a cache entry is garbage collected, delete the key from `perQueryReferences`.
132
+ cache.delete(queryString);
133
+ perQueryReferences.delete(queryString);
134
+ });
135
+
136
+ for (const table of tableNames) {
137
+ perTableResolver.set(table, promiseWithResolvers<void>());
138
+ }
139
+
140
+ const parseViewPromise = (async () => {
141
+ const unresolvedViewRelations = new Map<string, Set<string>>();
142
+ for (const view of views) {
143
+ const query = dialect.sqlToQuery(getViewConfig(view as PgView).query!);
144
+ const relations = await getSQLQueryRelations(query.sql);
145
+
146
+ unresolvedViewRelations.set(getViewName(view), relations);
147
+ }
148
+
149
+ /**
150
+ * Recursively resolve nested views (views that reference other views).
151
+ *
152
+ * @dev This assumes views cannot be infinitely cursive - an invariant enforced by Postgres.
153
+ */
154
+ const resolveRelation = (relation: string): Set<string> => {
155
+ if (perViewTables.has(relation)) {
156
+ return perViewTables.get(relation)!;
157
+ }
158
+
159
+ if (tableNames.has(relation)) {
160
+ return new Set([relation]);
161
+ }
162
+
163
+ if (viewNames.has(relation)) {
164
+ const result = new Set<string>();
165
+ for (const _relation of unresolvedViewRelations.get(relation)!) {
166
+ for (const __relation of resolveRelation(_relation)) {
167
+ result.add(__relation);
168
+ }
169
+ }
170
+ return result;
171
+ }
172
+
173
+ return new Set();
174
+ };
175
+
176
+ for (const [viewName, relations] of unresolvedViewRelations) {
177
+ const resolvedRelations = new Set<string>();
178
+ for (const relation of relations) {
179
+ for (const _relation of resolveRelation(relation)) {
180
+ resolvedRelations.add(_relation);
181
+ }
182
+ }
183
+ perViewTables.set(viewName, resolvedRelations);
184
+ }
185
+ })();
53
186
 
54
187
  if (driver.dialect === "pglite") {
55
- driver.instance.query(`LISTEN "${channel}"`).then(() => {
56
- driver.instance.onNotification(() => {
57
- // Clear cache on status change
58
- cache.clear();
59
- statusResolver.resolve();
60
- statusResolver = promiseWithResolvers<void>();
61
- });
188
+ const channel = getLiveQueryChannelName(
189
+ globalThis.PONDER_NAMESPACE_BUILD.schema,
190
+ );
191
+ driver.instance.query(`LISTEN "${channel}"`);
192
+
193
+ driver.instance.onNotification((_, payload) => {
194
+ const tables = JSON.parse(payload!) as string[];
195
+ tables.push("_ponder_checkpoint");
196
+ let invalidQueryCount = 0;
197
+
198
+ for (const [queryString, referencedTables] of perQueryReferences) {
199
+ let isQueryInvalid = false;
200
+ for (const table of tables) {
201
+ if (referencedTables.has(table)) {
202
+ isQueryInvalid = true;
203
+ break;
204
+ }
205
+ }
206
+
207
+ if (isQueryInvalid) {
208
+ invalidQueryCount++;
209
+
210
+ const resultPromise = cache.get(queryString)?.deref();
211
+ if (resultPromise) registry.unregister(resultPromise);
212
+
213
+ cache.delete(queryString);
214
+ perQueryReferences.delete(queryString);
215
+ }
216
+ }
217
+
218
+ for (const table of tables) {
219
+ perTableResolver.get(table)!.resolve();
220
+ perTableResolver.set(table, promiseWithResolvers<void>());
221
+ }
222
+
223
+ if (invalidQueryCount > 0) {
224
+ globalThis.PONDER_COMMON.logger.debug({
225
+ msg: "Updated live queries",
226
+ tables: JSON.stringify(Array.from(tables)),
227
+ query_count: invalidQueryCount,
228
+ });
229
+ }
62
230
  });
63
231
  } else {
64
232
  (async () => {
@@ -84,11 +252,46 @@ export const client = ({
84
252
  msg: `Established listen connection for "@ponder/client" middleware`,
85
253
  });
86
254
 
87
- client.on("notification", () => {
88
- // Clear cache on status change
89
- cache.clear();
90
- statusResolver.resolve();
91
- statusResolver = promiseWithResolvers<void>();
255
+ client.on("notification", (notification) => {
256
+ const tables = JSON.parse(notification.payload!) as string[];
257
+ tables.push("_ponder_checkpoint");
258
+ let invalidQueryCount = 0;
259
+
260
+ for (const [
261
+ queryString,
262
+ referencedTables,
263
+ ] of perQueryReferences) {
264
+ let isQueryInvalid = false;
265
+ for (const table of tables) {
266
+ if (referencedTables.has(table)) {
267
+ isQueryInvalid = true;
268
+ break;
269
+ }
270
+ }
271
+
272
+ if (isQueryInvalid) {
273
+ invalidQueryCount++;
274
+
275
+ const resultPromise = cache.get(queryString)?.deref();
276
+ if (resultPromise) registry.unregister(resultPromise);
277
+
278
+ cache.delete(queryString);
279
+ perQueryReferences.delete(queryString);
280
+ }
281
+ }
282
+
283
+ for (const table of tables) {
284
+ perTableResolver.get(table)!.resolve();
285
+ perTableResolver.set(table, promiseWithResolvers<void>());
286
+ }
287
+
288
+ if (invalidQueryCount > 0) {
289
+ globalThis.PONDER_COMMON.logger.debug({
290
+ msg: "Updated live queries",
291
+ tables: JSON.stringify(tables),
292
+ query_count: invalidQueryCount,
293
+ });
294
+ }
92
295
  });
93
296
 
94
297
  client.on("error", async (error) => {
@@ -105,6 +308,10 @@ export const client = ({
105
308
  resolve();
106
309
  });
107
310
 
311
+ const channel = getLiveQueryChannelName(
312
+ globalThis.PONDER_NAMESPACE_BUILD.schema,
313
+ );
314
+
108
315
  await client.query(`LISTEN "${channel}"`);
109
316
  } catch (error) {
110
317
  globalThis.PONDER_COMMON.logger.warn({
@@ -124,7 +331,25 @@ export const client = ({
124
331
  })();
125
332
  }
126
333
 
334
+ const getQueryResult = (query: QueryWithTypings): Promise<QueryResult> => {
335
+ if (driver.dialect === "pglite") {
336
+ return session.prepareQuery(query, undefined, undefined, false).execute();
337
+ } else {
338
+ return globalThis.PONDER_DATABASE.readonlyQB.raw.transaction(
339
+ (tx) => {
340
+ return tx._.session
341
+ .prepareQuery(query, undefined, undefined, false)
342
+ .execute();
343
+ },
344
+ { accessMode: "read only" },
345
+ );
346
+ }
347
+ };
348
+
127
349
  return createMiddleware(async (c, next) => {
350
+ const crypto = await import(/* webpackIgnore: true */ "node:crypto");
351
+ await parseViewPromise;
352
+
128
353
  if (c.req.path === "/sql/db") {
129
354
  const queryString = c.req.query("sql");
130
355
  if (queryString === undefined) {
@@ -133,43 +358,45 @@ export const client = ({
133
358
  const query = superjson.parse(queryString) as QueryWithTypings;
134
359
 
135
360
  try {
136
- await validateQuery(query.sql);
361
+ await validateAllowableSQLQuery(query.sql);
137
362
  } catch (error) {
138
363
  (error as Error).stack = undefined;
139
364
  return c.text((error as Error).message, 500);
140
365
  }
141
366
 
142
- let resultPromise: Promise<unknown>;
367
+ const relations = await getSQLQueryRelations(query.sql);
368
+ const referencedTables = new Set<string>();
369
+ for (const relation of relations) {
370
+ if (tableNames.has(relation)) {
371
+ referencedTables.add(relation);
372
+ } else if (viewNames.has(relation)) {
373
+ for (const tableName of perViewTables.get(relation)!) {
374
+ referencedTables.add(tableName);
375
+ }
376
+ }
377
+ }
143
378
 
144
- const _resultPromise = cache.get(queryString)?.deref() ?? undefined;
145
- if (_resultPromise === undefined) {
146
- cache.delete(queryString);
379
+ let resultPromise: Promise<unknown>;
147
380
 
148
- const pwr = promiseWithResolvers<unknown>();
149
- cache.set(queryString, new WeakRef(pwr.promise));
150
- resultPromise = pwr.promise;
381
+ if (isReady === false) {
382
+ resultPromise = getQueryResult(query);
383
+ } else if (cache.has(queryString)) {
384
+ const resultRef = cache.get(queryString)!.deref();
151
385
 
152
- if (driver.dialect === "pglite") {
153
- session
154
- .prepareQuery(query, undefined, undefined, false)
155
- .execute()
156
- .then(pwr.resolve)
157
- .catch(pwr.reject);
386
+ if (resultRef === undefined) {
387
+ cache.delete(queryString);
388
+ resultPromise = getQueryResult(query);
389
+ cache.set(queryString, new WeakRef(resultPromise));
390
+ perQueryReferences.set(queryString, referencedTables);
391
+ registry.register(resultPromise, queryString);
158
392
  } else {
159
- globalThis.PONDER_DATABASE.readonlyQB.raw
160
- .transaction(
161
- (tx) => {
162
- return tx._.session
163
- .prepareQuery(query, undefined, undefined, false)
164
- .execute();
165
- },
166
- { accessMode: "read only" },
167
- )
168
- .then(pwr.resolve)
169
- .catch(pwr.reject);
393
+ resultPromise = resultRef;
170
394
  }
171
395
  } else {
172
- resultPromise = _resultPromise;
396
+ resultPromise = getQueryResult(query);
397
+ cache.set(queryString, new WeakRef(resultPromise));
398
+ perQueryReferences.set(queryString, referencedTables);
399
+ registry.register(resultPromise, queryString);
173
400
  }
174
401
 
175
402
  try {
@@ -181,16 +408,127 @@ export const client = ({
181
408
  }
182
409
 
183
410
  if (c.req.path === "/sql/live") {
411
+ if (isReady === false) {
412
+ return c.text(
413
+ "Live queries are not available until the backfill is complete",
414
+ 503,
415
+ );
416
+ }
417
+
418
+ if (liveQueryCount >= MAX_LIVE_QUERIES) {
419
+ return c.text("Maximum number of live queries reached", 503);
420
+ }
421
+
422
+ liveQueryCount++;
423
+
184
424
  c.header("Content-Type", "text/event-stream");
185
425
  c.header("Cache-Control", "no-cache");
186
426
  c.header("Connection", "keep-alive");
187
427
 
428
+ const queryString = c.req.query("sql");
429
+ if (queryString === undefined) {
430
+ return c.text('Missing "sql" query parameter', 400);
431
+ }
432
+ const query = superjson.parse(queryString) as QueryWithTypings;
433
+
434
+ try {
435
+ await validateAllowableSQLQuery(query.sql);
436
+ } catch (error) {
437
+ (error as Error).stack = undefined;
438
+ return c.text((error as Error).message, 500);
439
+ }
440
+
441
+ const relations = await getSQLQueryRelations(query.sql);
442
+ const referencedTables = new Set<string>();
443
+ for (const relation of relations) {
444
+ if (tableNames.has(relation)) {
445
+ referencedTables.add(relation);
446
+ } else if (viewNames.has(relation)) {
447
+ for (const tableName of perViewTables.get(relation)!) {
448
+ referencedTables.add(tableName);
449
+ }
450
+ }
451
+ }
452
+
453
+ let result: QueryResult;
454
+ if (cache.has(queryString)) {
455
+ const resultRef = cache.get(queryString)!.deref();
456
+
457
+ if (resultRef === undefined) {
458
+ cache.delete(queryString);
459
+ const resultPromise = getQueryResult(query);
460
+ cache.set(queryString, new WeakRef(resultPromise));
461
+ perQueryReferences.set(queryString, referencedTables);
462
+ registry.register(resultPromise, queryString);
463
+ result = await resultPromise;
464
+ } else {
465
+ result = await resultRef;
466
+ }
467
+ } else {
468
+ const resultPromise = getQueryResult(query);
469
+ cache.set(queryString, new WeakRef(resultPromise));
470
+ perQueryReferences.set(queryString, referencedTables);
471
+ registry.register(resultPromise, queryString);
472
+ result = await resultPromise;
473
+ }
474
+
475
+ let resultHash = crypto
476
+ .createHash("MD5")
477
+ // @ts-ignore
478
+ .update(JSON.stringify(result.rows))
479
+ .digest("hex")
480
+ .slice(0, 10);
481
+
188
482
  return streamSSE(c, async (stream) => {
483
+ stream.onAbort(() => {
484
+ liveQueryCount--;
485
+ });
486
+
189
487
  while (stream.closed === false && stream.aborted === false) {
488
+ await Promise.race(
489
+ Array.from(referencedTables).map(
490
+ (relation) => perTableResolver.get(relation)!.promise,
491
+ ),
492
+ );
493
+
190
494
  try {
191
- await stream.writeSSE({ data: "" });
192
- } catch {}
193
- await statusResolver.promise;
495
+ let resultPromise: Promise<unknown>;
496
+ if (cache.has(queryString)) {
497
+ const resultRef = cache.get(queryString)!.deref();
498
+
499
+ if (resultRef === undefined) {
500
+ cache.delete(queryString);
501
+ resultPromise = getQueryResult(query);
502
+ cache.set(queryString, new WeakRef(resultPromise));
503
+ perQueryReferences.set(queryString, referencedTables);
504
+ registry.register(resultPromise, queryString);
505
+ } else {
506
+ resultPromise = resultRef;
507
+ }
508
+ } else {
509
+ resultPromise = getQueryResult(query);
510
+ cache.set(queryString, new WeakRef(resultPromise));
511
+ perQueryReferences.set(queryString, referencedTables);
512
+ registry.register(resultPromise, queryString);
513
+ }
514
+
515
+ const result = await resultPromise;
516
+
517
+ const _resultHash = crypto
518
+ .createHash("MD5")
519
+ // @ts-ignore
520
+ .update(JSON.stringify(result.rows))
521
+ .digest("hex")
522
+ .slice(0, 10);
523
+
524
+ if (_resultHash === resultHash) continue;
525
+ resultHash = _resultHash;
526
+
527
+ // @ts-ignore
528
+ await stream.writeSSE({ data: JSON.stringify(result) });
529
+ } catch {
530
+ stream.abort();
531
+ }
194
532
  }
195
533
  });
196
534
  }
@@ -7,7 +7,7 @@ import type { GetEventFilter } from "./eventFilter.js";
7
7
 
8
8
  export type Config = {
9
9
  database?: DatabaseConfig;
10
- ordering?: "omnichain" | "multichain";
10
+ ordering?: "omnichain" | "multichain" | "experimental_isolated";
11
11
  chains: { [chainName: string]: ChainConfig<unknown> };
12
12
  contracts: { [contractName: string]: GetContract };
13
13
  accounts: { [accountName: string]: AccountConfig<unknown> };
@@ -18,7 +18,7 @@ export type Config = {
18
18
 
19
19
  export type CreateConfigReturnType<chains, contracts, accounts, blocks> = {
20
20
  database?: DatabaseConfig;
21
- ordering?: "omnichain" | "multichain";
21
+ ordering?: "omnichain" | "multichain" | "experimental_isolated";
22
22
  chains: chains;
23
23
  contracts: contracts;
24
24
  accounts: accounts;
@@ -32,7 +32,7 @@ export const createConfig = <
32
32
  const blocks = {},
33
33
  >(config: {
34
34
  database?: DatabaseConfig;
35
- ordering?: "omnichain" | "multichain";
35
+ ordering?: "omnichain" | "multichain" | "experimental_isolated";
36
36
  // TODO: add jsdoc to these properties.
37
37
  chains: ChainsConfig<Narrow<chains>>;
38
38
  contracts?: ContractsConfig<chains, Narrow<contracts>>;