@effectstream/runtime 0.100.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.
package/src/main.ts ADDED
@@ -0,0 +1,317 @@
1
+ import {
2
+ type AllSyncProtocols,
3
+ type ChainBlock,
4
+ genSyncProtocols,
5
+ } from "@effectstream/sync";
6
+ import {
7
+ acquireDBMutex,
8
+ createDynamicTables,
9
+ getConnection,
10
+ releaseDBMutex,
11
+ resetPublicTables,
12
+ runSnapshotLoop,
13
+ } from "@effectstream/db";
14
+ import { EventBroker } from "@effectstream/event-server";
15
+ import {
16
+ BuiltinEvents,
17
+ EventManager,
18
+ } from "@effectstream/event-client";
19
+ import { startMerge, startSync } from "@effectstream/sync";
20
+ import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
21
+ import {
22
+ createChannel,
23
+ each,
24
+ type Operation,
25
+ sleep,
26
+ spawn,
27
+ until,
28
+ } from "effection";
29
+ import { initTelemetry } from "./telemetry.ts";
30
+ import { processFinalizedBlock } from "./process-blocks.ts";
31
+ import { startHttpServer } from "./api/http-server.ts";
32
+ import type { StartConfig } from "./types.ts";
33
+ import type { Client } from "pg";
34
+ import type { EffectstreamBlockHash } from "@effectstream/utils";
35
+ import { applySystemMigrations } from "./version-migrations.ts";
36
+ import { getLastBlockHeight, getVersionInfo } from "@effectstream/db/version";
37
+ import { type SyncProtocolWithNetwork, ConfigNetworkType } from "@effectstream/config";
38
+ import { builtInPrimitivesMap } from "@effectstream/sm";
39
+ import { validateAndSnapshotConfig } from "./config-snapshot.ts";
40
+
41
+ export function* init() {
42
+ // Prevent transient network errors (broken pipes, closed connections) from
43
+ // crashing the process via unhandled promise rejections. The sync loop's
44
+ // tryYield already retries on error — this just keeps the process alive.
45
+ globalThis.addEventListener("unhandledrejection", (event) => {
46
+ log.local(
47
+ ComponentNames.EFFECTSTREAM_RUNTIME,
48
+ "unhandled-rejection",
49
+ SeverityNumber.WARN,
50
+ (l) => l("Suppressed unhandled rejection:", event.reason),
51
+ );
52
+ event.preventDefault();
53
+ });
54
+
55
+ // initialize OpenTelemetry
56
+ yield* initTelemetry();
57
+ }
58
+
59
+ /**
60
+ * Main entry point to start the Paima Engine Node.
61
+ *
62
+ * This will launch the networks/primitives synchronization sub-processes,
63
+ * the HTTP server, and the merge and effectstream-block generation process.
64
+ *
65
+ * @param config - Paima Engine Node configuration object.
66
+ */
67
+ export function* start(config: StartConfig): Operation<void> {
68
+ const { syncInfo } = config;
69
+
70
+ const dbConn = getConnection();
71
+
72
+ const syncProtocols = yield* startup(dbConn as any, // Client,
73
+ syncInfo, config);
74
+
75
+ log.remote(
76
+ ComponentNames.EFFECTSTREAM_RUNTIME,
77
+ [],
78
+ SeverityNumber.INFO,
79
+ (log) => log("start sync", syncProtocols.map(p => p.name)),
80
+ );
81
+ for (const syncProtocol of syncProtocols) {
82
+ yield* startSync(syncProtocol);
83
+ }
84
+
85
+ // Create MQTT Broker
86
+ new EventBroker("effectstream-engine").createServer();
87
+
88
+ yield* spawn(function* () {
89
+ yield* startHttpServer(
90
+ dbConn,
91
+ syncProtocols,
92
+ config.apiRouter,
93
+ config.grammar,
94
+ );
95
+ });
96
+
97
+ const ntpConfig = syncInfo.find(s => s.networkType === ConfigNetworkType.NTP);
98
+ const ntpBlockTimeMS = (ntpConfig?.network as { blockTimeMS: number } | undefined)?.blockTimeMS;
99
+ const lagThresholdMs = ntpBlockTimeMS != null ? ntpBlockTimeMS * 10 : undefined;
100
+
101
+ const finalizedBlockStream = createChannel<ChainBlock>();
102
+
103
+ yield* spawn(() => startMerge(syncProtocols, finalizedBlockStream));
104
+
105
+ const heartbeatIntervalMs = 60_000;
106
+ yield* spawn(function* () {
107
+ while (true) {
108
+ yield* sleep(heartbeatIntervalMs);
109
+ const now = Date.now();
110
+ const status = syncProtocols.map((p) => {
111
+ const page = p.lastPage;
112
+ if (page == null) return `${p.name}: waiting for first sync`;
113
+ const ageMs = now - (page.root as number);
114
+ let line = `${p.name}: block ${page.ownBlockNumber} | buf ${p.bufferedData.size()} | age ${(ageMs / 1000).toFixed(1)}s`;
115
+ if (p.consecutiveErrors > 0) {
116
+ const sinceLast = p.lastErrorTimestamp > 0
117
+ ? ` ${((now - p.lastErrorTimestamp) / 1000).toFixed(0)}s ago`
118
+ : "";
119
+ line += ` | ERRORS: ${p.consecutiveErrors}${sinceLast}`;
120
+ } else if (p.lastSuccessfulFetchMs > 0) {
121
+ const idleMs = now - p.lastSuccessfulFetchMs;
122
+ if (idleMs > heartbeatIntervalMs * 2) {
123
+ line += ` | IDLE: ${(idleMs / 1000).toFixed(0)}s`;
124
+ }
125
+ }
126
+ return line;
127
+ });
128
+ log.local(
129
+ ComponentNames.EFFECTSTREAM_SYNC,
130
+ "heartbeat",
131
+ SeverityNumber.INFO,
132
+ (l) => l(status.join(" | ")),
133
+ );
134
+ }
135
+ });
136
+
137
+ let blockHash: EffectstreamBlockHash | null = null;
138
+ if (config.snapshotConfig) {
139
+ yield* spawn(() => runSnapshotLoop(config.snapshotConfig!));
140
+ }
141
+
142
+ for (const value of yield* each(finalizedBlockStream)) {
143
+ // We request a dbClient for a non-shared dbConn object.
144
+ // For PGLite, this is not enough, as the can only be one connection at a time.
145
+ // So we request a DBMutex as well.
146
+ let dbClient: Client | undefined;
147
+ try {
148
+ yield* acquireDBMutex(`processing-blocks:${value.blockNumber}`);
149
+ dbClient = yield* until((dbConn as any).connect()); // Client,
150
+
151
+ blockHash = yield* processFinalizedBlock(
152
+ value,
153
+ config,
154
+ dbClient as any, // Client,
155
+ blockHash,
156
+ );
157
+ } finally {
158
+ releaseDBMutex(`processing-blocks:${value.blockNumber}`);
159
+ if (dbClient) {
160
+ (dbClient as any).release(); // Client,
161
+ }
162
+ }
163
+
164
+ // Used to emit & log the block range for each protocol.
165
+ const contentBlocksForProtocol = getRangesForSyncProtocols(value);
166
+
167
+ yield* until(
168
+ emitLatestBlocks(
169
+ value.blockNumber,
170
+ value.timestamp,
171
+ contentBlocksForProtocol,
172
+ ),
173
+ );
174
+
175
+ const lagMs = Date.now() - value.timestamp;
176
+ const lagSuffix = lagThresholdMs != null && lagMs > lagThresholdMs
177
+ ? ` | lag: ${(lagMs / 1000).toFixed(1)}s`
178
+ : "";
179
+ log.local(
180
+ ComponentNames.EFFECTSTREAM_SYNC,
181
+ "block-merge",
182
+ SeverityNumber.INFO,
183
+ (log) =>
184
+ log(
185
+ `finalized block ${value.blockNumber} @ ${
186
+ blockHash?.slice(0, 8)
187
+ }...${lagSuffix} | ${JSON.stringify(contentBlocksForProtocol)}`,
188
+ ),
189
+ );
190
+ yield* each.next();
191
+ }
192
+ }
193
+
194
+ function getRangesForSyncProtocols(value: ChainBlock): Record<string, [number, number]> {
195
+ const contentBlocksForProtocol: Record<string, [number, number]> = {};
196
+ for (const block of value.blockInfo) {
197
+ if (!contentBlocksForProtocol[block.protocol_name]) {
198
+ contentBlocksForProtocol[block.protocol_name] = [block.block_number, block.block_number];
199
+ }
200
+ contentBlocksForProtocol[block.protocol_name] = [
201
+ Math.min(contentBlocksForProtocol[block.protocol_name][0], block.block_number),
202
+ Math.max(contentBlocksForProtocol[block.protocol_name][1], block.block_number),
203
+ ];
204
+ }
205
+ return contentBlocksForProtocol;
206
+ }
207
+
208
+ async function emitLatestBlocks(
209
+ rollUpBlockHeight: number,
210
+ rollUpBlockTimestamp: number,
211
+ syncChains: Record<string, [number, number]>,
212
+ ) {
213
+ return await Promise.all([
214
+ EventManager.Instance.sendMessage(BuiltinEvents.RollupBlock, {
215
+ block: rollUpBlockHeight,
216
+ timestamp: rollUpBlockTimestamp,
217
+ }),
218
+ ...Object.entries(syncChains).map(([chainName, [_, toBlock]]) =>
219
+ EventManager.Instance.sendMessage(BuiltinEvents.SyncChains, {
220
+ chain: chainName,
221
+ block: toBlock,
222
+ rollup: rollUpBlockHeight
223
+ })
224
+ ),
225
+ ]);
226
+ }
227
+
228
+ function* startup(
229
+ dbConn: Client,
230
+ syncInfo: SyncProtocolWithNetwork[],
231
+ config: StartConfig,
232
+ ): Operation<AllSyncProtocols[]> {
233
+ const versionInfo = yield* getVersionInfo(dbConn);
234
+ const lastBlockHeight = yield* getLastBlockHeight(versionInfo, dbConn);
235
+ // Create Runtime Primitives Instances
236
+ syncInfo.forEach((syncProtocol) => {
237
+ syncProtocol.primitives.forEach((primitive, primitiveIndex) => {
238
+ processPrimitives(
239
+ syncProtocol.primitives,
240
+ primitiveIndex,
241
+ config.userDefinedPrimitives
242
+ );
243
+ });
244
+ });
245
+
246
+
247
+ yield* acquireDBMutex(`startup-node`);
248
+
249
+ // Dev-only reset of user-owned public tables
250
+ if (config.dev?.resetPublicData) {
251
+ yield* resetPublicTables(dbConn as any); // Client,
252
+ }
253
+
254
+ // When the node is started, we apply system migrations.
255
+ // Either system initial migrations, or migrations given a Paima Engine Update.
256
+ yield* applySystemMigrations(
257
+ config.appVersion,
258
+ versionInfo,
259
+ lastBlockHeight,
260
+ dbConn,
261
+ config.migrations,
262
+ );
263
+
264
+ // Validate that immutable config fields (e.g. NTP startTime, startBlockHeight)
265
+ // have not changed since the last run. Persists a snapshot on first start.
266
+ // Must run after system migrations so the snapshot table exists.
267
+ yield* validateAndSnapshotConfig(syncInfo, dbConn);
268
+
269
+ const syncProtocols = yield* genSyncProtocols(dbConn as any, // Client,
270
+ syncInfo);
271
+
272
+ yield* createDynamicTables(
273
+ versionInfo,
274
+ lastBlockHeight,
275
+ dbConn as any, // Client,
276
+ syncProtocols,
277
+ );
278
+
279
+ releaseDBMutex(`startup-node`);
280
+ return syncProtocols;
281
+ }
282
+
283
+ // Convert the primitive config to the final primitive instance
284
+ const processPrimitives = (
285
+ primitives: {primitive: any, id: string}[],
286
+ primitiveIndex: number,
287
+ userDefinedPrimitives?: Record<string, any>,
288
+ ) => {
289
+ const primitiveType = primitives[primitiveIndex].primitive.type;
290
+ const primitiveUniqueName = primitives[primitiveIndex].id;
291
+ const primitiveConfig = primitives[primitiveIndex].primitive;
292
+ const isBuiltInPrimitive = primitiveType in builtInPrimitivesMap;
293
+ const isUserDefinedPrimitive = userDefinedPrimitives && primitiveType in userDefinedPrimitives;
294
+ if (isBuiltInPrimitive && isUserDefinedPrimitive) {
295
+ throw new Error(`User defined primitive cannot have the same name as a built-in primitive.
296
+ Built-in values: ${Object.keys(builtInPrimitivesMap).join(", ")}`);
297
+ }
298
+ if (!isBuiltInPrimitive && !isUserDefinedPrimitive) {
299
+ throw new Error(`PrimitiveUniqueName "${primitiveUniqueName}" is not built-in and not user-defined.
300
+ Available values: ${Object.keys([
301
+ ...Object.keys(builtInPrimitivesMap),
302
+ ...Object.keys(userDefinedPrimitives || {}),
303
+ ]).join(", ")}`);
304
+ }
305
+ let p = null;
306
+ const classConfig = {
307
+ ...primitiveConfig,
308
+ instanceName: primitiveUniqueName,
309
+ }
310
+ if (isBuiltInPrimitive) {
311
+ p = new builtInPrimitivesMap[primitiveType as keyof typeof builtInPrimitivesMap](classConfig as any) ;
312
+ } else if (isUserDefinedPrimitive) {
313
+ p = new userDefinedPrimitives[primitiveType as keyof typeof userDefinedPrimitives](classConfig);
314
+ }
315
+ // Update the primitive with the final configuration
316
+ primitives[primitiveIndex].primitive = p.getConfig();
317
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { init, start } from "./main.ts";
2
+ export * from "./types.ts";
3
+ export * from "./api/pagination.ts";
@@ -0,0 +1,302 @@
1
+ import type { ChainBlock } from "@effectstream/sync";
2
+ import { call, type Operation, until } from "effection";
3
+ import type { Pool } from "pg";
4
+ import { type BaseStfInput, primitiveTransitionFunction } from "@effectstream/sm";
5
+ import { PreparedQuery } from "@pgtyped/runtime";
6
+ import type {
7
+ ExecPromise,
8
+ QueuedUpdate,
9
+ SyncStateUpdateStream,
10
+ } from "@effectstream/coroutine";
11
+ import {
12
+ blockHeightDone,
13
+ deleteScheduled,
14
+ getFutureGameInputByBlockHeight,
15
+ getFutureGameInputByMaxTimestamp,
16
+ insertGameInputResult,
17
+ removePages,
18
+ saveLastBlock,
19
+ } from "@effectstream/db";
20
+ import { Buffer } from "node:buffer";
21
+ import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
22
+ import type { StartConfig } from "./types.ts";
23
+ import type { BlockNumber, EffectstreamBlockHash } from "@effectstream/utils";
24
+ import { generateEffectstreamBlockHash, Prando } from "@effectstream/crypto";
25
+ import { applyUserMigrations } from "./version-migrations.ts";
26
+
27
+ /** Helper to check if a SyncStateUpdateStream object is a WorldResolve */
28
+ function isWorldResolve(value: any): value is QueuedUpdate {
29
+ return value && Array.isArray(value);
30
+ }
31
+ /** Helper to check if a SyncStateUpdateStream object is a promise */
32
+ function isPromise(value: any): value is ExecPromise<any> {
33
+ return value && typeof value === "object" && "promise" in value;
34
+ }
35
+
36
+ /**
37
+ * We need to process all the SQL calls of an STF update in an all-or-nothing manner
38
+ * STF updates can fail (since the data for them comes from arbitrary onchain data)
39
+ * But we can't allow a single user's bad transaction to DOS the game for everybody else
40
+ * So failures should be isolated to just the specific input, and not the full block
41
+ * (recall: without this, in pgsql, if a query fails during a db transaction, the entire dbTx becomes invalid)
42
+ */
43
+ async function tryOrRollback<T>(
44
+ dbTx: Pool,
45
+ func: () => Promise<T>,
46
+ ): Promise<{ status: "success" | "error"; value: T | undefined }> {
47
+ const checkpointName = `game_state_transition`;
48
+ await dbTx.query(`SAVEPOINT ${checkpointName}`);
49
+ try {
50
+ const value = await func();
51
+ return { status: "success", value };
52
+ } catch (err) {
53
+ await dbTx.query(`ROLLBACK TO SAVEPOINT ${checkpointName}`);
54
+ log.remote(
55
+ ComponentNames.EFFECTSTREAM_SYNC,
56
+ "block-processing",
57
+ SeverityNumber.INFO,
58
+ (log) =>
59
+ log(`Database error on ${checkpointName}. Rolling back.` + String(err)),
60
+ );
61
+ return { status: "error", value: undefined };
62
+ } finally {
63
+ await dbTx.query(`RELEASE SAVEPOINT ${checkpointName}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * This function is used to execute a generator step by step.
69
+ * Each step returns either a Query or a StateMachineExecution.
70
+ * The results are processed, then the generator is resumed.
71
+ */
72
+ function* executeGeneratorStepByStep(
73
+ generator: SyncStateUpdateStream<void>,
74
+ dbConn: Pool,
75
+ ): Operation<any> {
76
+ let result = generator.next();
77
+
78
+ // NOTE: sync generators cannot execute promises
79
+ // so we pause the execution, and resolve them here.
80
+ // NOTE: there are 2 types of operations we need to handle
81
+ // 1. state machine executions, these return a series of queries to run.
82
+ // 2. generic promises required in the primitives (e.g., verifySignature)
83
+ while (!result.done) {
84
+ // We resolve the generators promises here.
85
+ // As generators cannot execute promises.
86
+ const operations: unknown[] = [];
87
+ const value = result.value;
88
+
89
+ if (isWorldResolve(value)) {
90
+ const [queryIR, params] = value;
91
+ const query = new PreparedQuery(queryIR);
92
+ const queryResult = yield* call(() => query.run(params, dbConn));
93
+ operations.push(queryResult);
94
+ } else if (isPromise(value)) {
95
+ // This means that we have non-database promises in the generator.
96
+ // Each time yield is called, we catch the intention and resolve it.
97
+ const queryResult = yield* until(value.promise);
98
+ // We wrap the result, but it will be flattened by the generator.
99
+ operations.push([queryResult]);
100
+ } else {
101
+ console.error("Yielding unhandled type", result);
102
+ throw new Error("Unhandled type in generator");
103
+ }
104
+ result = generator.next(operations.flat());
105
+ }
106
+ return result.value;
107
+ }
108
+
109
+ /**
110
+ * This function is the main entry point for processing a produced block.
111
+ * It is called when a block can be processed and finalized.
112
+ * It runs the entire pipeline in a transaction, with sub-transactions for each StateMachineExecution.
113
+ *
114
+ * Process Order:
115
+ * 1. Create a temporal block record
116
+ * 2. Process the migrations for this block height
117
+ * 4. Extract the scheduled data from the primitives in the block
118
+ * 4. Process the scheduled data for this block height, and time
119
+ * 5. Mark the block as done, and add the hash.
120
+ * 6. Commit the transaction
121
+ *
122
+ * IMPORTANT: This function must receive a non-shared dbConn object.
123
+ * as it will be used in a transaction.
124
+ */
125
+ export function* processFinalizedBlock(
126
+ value: ChainBlock,
127
+ config: StartConfig,
128
+ dbConn: Pool,
129
+ previousBlockHash: EffectstreamBlockHash | null,
130
+ ): Operation<EffectstreamBlockHash> {
131
+ const { gameStateTransitions, migrations } = config;
132
+ const blockHash: EffectstreamBlockHash = generateEffectstreamBlockHash(
133
+ value,
134
+ previousBlockHash,
135
+ );
136
+ try {
137
+ /* STEP 0: Start the transaction. */
138
+ yield* until(dbConn.query("BEGIN"));
139
+ const randomGenerator = new Prando(blockHash);
140
+
141
+ /* STEP 1: Create a temporal block record */
142
+ yield* call(() =>
143
+ saveLastBlock.run({
144
+ // TODO: Check these values
145
+ block_height: value.blockNumber,
146
+ ver: 0,
147
+ main_chain_block_hash: Buffer.from(value.blockNumber.toString()),
148
+ seed: randomGenerator.seed.toString(),
149
+ ms_timestamp: new Date(value.timestamp),
150
+ }, dbConn)
151
+ );
152
+
153
+ /* STEP 2: Process the migrations. */
154
+ if (migrations) {
155
+ yield* applyUserMigrations(
156
+ value.blockNumber,
157
+ dbConn as any, // Client,
158
+ migrations,
159
+ );
160
+ }
161
+
162
+ /* STEP 3: Process the primitives. */
163
+ for (const primitive of value.primitives) {
164
+ const generator = primitiveTransitionFunction(
165
+ value.blockNumber,
166
+ primitive,
167
+ );
168
+ yield* executeGeneratorStepByStep(generator, dbConn);
169
+ }
170
+
171
+ /* STEP 4: Extract the scheduled data. */
172
+ const scheduledData1 = yield* call(() =>
173
+ getFutureGameInputByBlockHeight.run({
174
+ block_height: value.blockNumber,
175
+ }, dbConn)
176
+ );
177
+ /* STEP 4.b: Extract the scheduled data by timestamp. */
178
+ const scheduledData2 = yield* call(() =>
179
+ getFutureGameInputByMaxTimestamp.run({
180
+ max_timestamp: new Date(value.timestamp),
181
+ }, dbConn)
182
+ );
183
+
184
+ /* STEP 5: Process the scheduled data in the State Machine. */
185
+ // TODO What should be the order of the scheduled data - per id?
186
+ const scheduledData = [...scheduledData1, ...scheduledData2];
187
+ let index_in_block = 0;
188
+
189
+ if (gameStateTransitions && scheduledData.length > 0) {
190
+ randomGenerator.skip();
191
+ for (const data of scheduledData) {
192
+ let success = true;
193
+ try {
194
+ const input: BaseStfInput = {
195
+ blockTimestamp: value.timestamp,
196
+ blockHeight: value.blockNumber,
197
+ conciseInput: data.input_data,
198
+ signerAddress: data.from_address,
199
+ signerAddressType: data.from_address_type,
200
+ randomGenerator,
201
+ // TODO: We might want to add this to the scheduled data.
202
+ //
203
+ // Or do we resolve it here. Account might change.
204
+ // Should we preserve the original accountId or the signer's
205
+ // current accountId?
206
+ accountId: undefined, // data.accountId,
207
+ };
208
+ const gameSTFGenerator = gameStateTransitions(
209
+ value.blockNumber,
210
+ input,
211
+ );
212
+ yield* executeGeneratorStepByStep(gameSTFGenerator, dbConn);
213
+ } catch (err) {
214
+ success = false;
215
+ log.remote(
216
+ ComponentNames.EFFECTSTREAM_SYNC,
217
+ "block-processing",
218
+ SeverityNumber.ERROR,
219
+ (log) =>
220
+ log(
221
+ `Error processing block ${value.blockNumber}: ${
222
+ String(err)
223
+ }. Payload: ${JSON.stringify(data)}`,
224
+ ),
225
+ );
226
+ }
227
+ const conciseInputHash = `0x${
228
+ Array(64).fill(0).map(() =>
229
+ Math.floor(Math.random() * 16).toString(16)
230
+ )
231
+ .join("")
232
+ }`;
233
+ yield* until(
234
+ insertGameInputResult.run({
235
+ id: data.id,
236
+ success,
237
+ effectstream_tx_hash: Buffer.from(conciseInputHash),
238
+ index_in_block,
239
+ block_height: value.blockNumber,
240
+ }, dbConn),
241
+ );
242
+ index_in_block++;
243
+
244
+ // Remove the scheduled data from the database.
245
+ yield* until(
246
+ deleteScheduled.run({
247
+ id: data.id,
248
+ }, dbConn),
249
+ );
250
+ }
251
+ }
252
+
253
+ /* STEP 6: Mark the block as done. */
254
+ yield* call(() =>
255
+ blockHeightDone.run({
256
+ block_hash: Buffer.from(blockHash),
257
+ block_height: value.blockNumber,
258
+ }, dbConn)
259
+ );
260
+
261
+ /* STEP 7: Update page state for all sync protocols consumed
262
+ * This is to mark what blocks where consumed by this specific
263
+ * block, to avoid re-processing the same blocks more than once
264
+ * if the sync protocol is restarted.
265
+ */
266
+
267
+ // Get latest block for each sync protocol.
268
+ const latestBlocks: Record<string, BlockNumber> = {};
269
+ for (const blockInfo of value.blockInfo) {
270
+ if (
271
+ !latestBlocks[blockInfo.protocol_name] ||
272
+ latestBlocks[blockInfo.protocol_name] < blockInfo.block_number
273
+ ) {
274
+ latestBlocks[blockInfo.protocol_name] = blockInfo.block_number;
275
+ }
276
+ }
277
+
278
+ for (const [protocolName, blockNumber] of Object.entries(latestBlocks)) {
279
+ yield* until(
280
+ removePages.run({
281
+ protocol_name: protocolName,
282
+ page_number: blockNumber,
283
+ }, dbConn),
284
+ );
285
+ }
286
+
287
+ /* STEP 8: Commit the transaction. */
288
+ yield* until(dbConn.query("COMMIT"));
289
+ } catch (err) {
290
+ yield* until(dbConn.query("ROLLBACK"));
291
+ log.remote(
292
+ ComponentNames.EFFECTSTREAM_SYNC,
293
+ "block-processing",
294
+ SeverityNumber.ERROR,
295
+ (log) =>
296
+ log(`Error processing block ${value.blockNumber}: ${String(err)}`),
297
+ );
298
+ // We cannot recover from this error.
299
+ throw err;
300
+ }
301
+ return blockHash;
302
+ }
@@ -0,0 +1,37 @@
1
+ import { NodeSDK } from "@opentelemetry/sdk-node";
2
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
3
+ import opentelemetry from "@opentelemetry/api";
4
+ import { defaultOtelSetup, PaimaTelemetryContext } from "@effectstream/log";
5
+ import type { Operation } from "effection";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { parse } from "jsonc-parser";
9
+
10
+ // TODO: this is hacky, but there is no proper way to do this unfortunately
11
+ // We're blocked by any of the following
12
+ // 1. Allowing `type: jsonc` imports: https://github.com/denoland/deno/issues/20374#issuecomment-2543118482
13
+ // 2. Allowing `type: tesxt` imports: https://github.com/denoland/deno/issues/25354
14
+ const version = "0.3.0";
15
+ // 3. import.meta.dirname is undefined when loaded as a jsr package
16
+ // const DenoConfig = parse(fs.readFileSync(
17
+ // path.resolve(import.meta.dirname!, "../", "./deno.json"),
18
+ // "utf-8",
19
+ // ));
20
+
21
+ export function* initTelemetry(): Operation<void> {
22
+ const sdk = new NodeSDK({
23
+ ...defaultOtelSetup("effectstream-node", version), // DenoConfig.version),
24
+ // TODO: set OTEL_EXPORTER_OTLP_PROTOCOL to json for @effectstream/collector support until it supports protobuf
25
+ instrumentations: [getNodeAutoInstrumentations()],
26
+ });
27
+
28
+ const tracer = opentelemetry.trace.getTracer("paima", version); // DenoConfig.version);
29
+ yield* PaimaTelemetryContext.set(
30
+ {
31
+ tracer,
32
+ span: undefined,
33
+ },
34
+ );
35
+
36
+ sdk.start();
37
+ }