@effectstream/runtime 0.100.12 → 0.100.14
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/package.json +12 -12
- package/src/main.ts +82 -39
- package/src/process-blocks.ts +59 -5
- package/src/version-migrations.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/runtime",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
},
|
|
14
14
|
"exports": "./src/mod.ts",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@effectstream/concise": "0.100.
|
|
17
|
-
"@effectstream/config": "0.100.
|
|
18
|
-
"@effectstream/coroutine": "0.100.
|
|
19
|
-
"@effectstream/crypto": "0.100.
|
|
20
|
-
"@effectstream/db": "0.100.
|
|
21
|
-
"@effectstream/event-client": "0.100.
|
|
22
|
-
"@effectstream/event-server": "0.100.
|
|
23
|
-
"@effectstream/log": "0.100.
|
|
24
|
-
"@effectstream/sm": "0.100.
|
|
25
|
-
"@effectstream/sync": "0.100.
|
|
26
|
-
"@effectstream/utils": "0.100.
|
|
16
|
+
"@effectstream/concise": "0.100.14",
|
|
17
|
+
"@effectstream/config": "0.100.14",
|
|
18
|
+
"@effectstream/coroutine": "0.100.14",
|
|
19
|
+
"@effectstream/crypto": "0.100.14",
|
|
20
|
+
"@effectstream/db": "0.100.14",
|
|
21
|
+
"@effectstream/event-client": "0.100.14",
|
|
22
|
+
"@effectstream/event-server": "0.100.14",
|
|
23
|
+
"@effectstream/log": "0.100.14",
|
|
24
|
+
"@effectstream/sm": "0.100.14",
|
|
25
|
+
"@effectstream/sync": "0.100.14",
|
|
26
|
+
"@effectstream/utils": "0.100.14",
|
|
27
27
|
"@fastify/cors": "^11.0.1",
|
|
28
28
|
"@metamask/json-rpc-engine": "^10.0.3",
|
|
29
29
|
"pg": "8.17.2",
|
package/src/main.ts
CHANGED
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
acquireDBMutex,
|
|
8
8
|
createDynamicTables,
|
|
9
9
|
getConnection,
|
|
10
|
+
getLastNonEmptyBlockHash,
|
|
10
11
|
releaseDBMutex,
|
|
11
12
|
resetPublicTables,
|
|
12
13
|
runSnapshotLoop,
|
|
13
14
|
} from "@effectstream/db";
|
|
14
15
|
import { EventBroker } from "@effectstream/event-server";
|
|
16
|
+
import { ENV } from "@effectstream/utils/node-env";
|
|
15
17
|
import {
|
|
16
18
|
BuiltinEvents,
|
|
17
19
|
EventManager,
|
|
@@ -27,7 +29,7 @@ import {
|
|
|
27
29
|
until,
|
|
28
30
|
} from "effection";
|
|
29
31
|
import { initTelemetry } from "./telemetry.ts";
|
|
30
|
-
import { processFinalizedBlock } from "./process-blocks.ts";
|
|
32
|
+
import { type PendingEvent, processFinalizedBlock } from "./process-blocks.ts";
|
|
31
33
|
import { startHttpServer } from "./api/http-server.ts";
|
|
32
34
|
import type { StartConfig } from "./types.ts";
|
|
33
35
|
import type { Client } from "pg";
|
|
@@ -39,19 +41,6 @@ import { builtInPrimitivesMap } from "@effectstream/sm";
|
|
|
39
41
|
import { validateAndSnapshotConfig } from "./config-snapshot.ts";
|
|
40
42
|
|
|
41
43
|
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
44
|
// initialize OpenTelemetry
|
|
56
45
|
yield* initTelemetry();
|
|
57
46
|
}
|
|
@@ -83,7 +72,9 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
83
72
|
}
|
|
84
73
|
|
|
85
74
|
// Create MQTT Broker
|
|
86
|
-
|
|
75
|
+
if (ENV.MQTT_BROKER) {
|
|
76
|
+
new EventBroker("effectstream-engine").createServer();
|
|
77
|
+
}
|
|
87
78
|
|
|
88
79
|
yield* spawn(function* () {
|
|
89
80
|
yield* startHttpServer(
|
|
@@ -134,7 +125,10 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
134
125
|
}
|
|
135
126
|
});
|
|
136
127
|
|
|
137
|
-
|
|
128
|
+
const [lastHashRow] = yield* until(getLastNonEmptyBlockHash.run(undefined, dbConn));
|
|
129
|
+
let blockHash: EffectstreamBlockHash | null = lastHashRow
|
|
130
|
+
? lastHashRow.effectstream_block_hash!.toString() as EffectstreamBlockHash
|
|
131
|
+
: null;
|
|
138
132
|
if (config.snapshotConfig) {
|
|
139
133
|
yield* spawn(() => runSnapshotLoop(config.snapshotConfig!));
|
|
140
134
|
}
|
|
@@ -144,16 +138,25 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
144
138
|
// For PGLite, this is not enough, as the can only be one connection at a time.
|
|
145
139
|
// So we request a DBMutex as well.
|
|
146
140
|
let dbClient: Client | undefined;
|
|
141
|
+
// Custom app events emitted by STFs during this block. Collected by
|
|
142
|
+
// processFinalizedBlock and flushed below ONLY after it returns — i.e.
|
|
143
|
+
// strictly after the block's COMMIT. See plan invariant I1.
|
|
144
|
+
let blockAppEvents: PendingEvent[] = [];
|
|
147
145
|
try {
|
|
148
146
|
yield* acquireDBMutex(`processing-blocks:${value.blockNumber}`);
|
|
149
147
|
dbClient = yield* until((dbConn as any).connect()); // Client,
|
|
150
148
|
|
|
151
|
-
|
|
149
|
+
const result = yield* processFinalizedBlock(
|
|
152
150
|
value,
|
|
153
151
|
config,
|
|
154
152
|
dbClient as any, // Client,
|
|
155
153
|
blockHash,
|
|
156
154
|
);
|
|
155
|
+
const resultHash = result.blockHash;
|
|
156
|
+
blockAppEvents = result.events;
|
|
157
|
+
if (resultHash !== "0x0") {
|
|
158
|
+
blockHash = resultHash;
|
|
159
|
+
}
|
|
157
160
|
} finally {
|
|
158
161
|
releaseDBMutex(`processing-blocks:${value.blockNumber}`);
|
|
159
162
|
if (dbClient) {
|
|
@@ -163,14 +166,38 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
163
166
|
|
|
164
167
|
// Used to emit & log the block range for each protocol.
|
|
165
168
|
const contentBlocksForProtocol = getRangesForSyncProtocols(value);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
|
|
170
|
+
// Fire-and-forget: do not stall the sync loop on broker acks.
|
|
171
|
+
//
|
|
172
|
+
// Opifex's TcpClient assigns packet IDs synchronously in client.publish()
|
|
173
|
+
// and writes to the socket via Node's net.Socket, which has its own write
|
|
174
|
+
// queue — so concurrent publish() calls land on the wire in call order.
|
|
175
|
+
// MQTT 3.1.1 §4.6 then guarantees in-order delivery to each subscriber
|
|
176
|
+
// per (publisher, topic, QoS). We don't need a wrapper queue.
|
|
177
|
+
//
|
|
178
|
+
// TODO(scaling): the broker runs in-process (see `new EventBroker(...)`
|
|
179
|
+
// above). Fan-out to N subscribers consumes the same event loop as block
|
|
180
|
+
// processing. At ~thousands of subscribers, consider:
|
|
181
|
+
// 1) moving the broker to a worker / separate process,
|
|
182
|
+
// 2) a publisher-side circuit breaker if ctx.unresolvedPublish.size > N
|
|
183
|
+
// (drop events with a counter for observability).
|
|
184
|
+
// Today: localhost, in-process, expected O(10²) subscribers — non-issue.
|
|
185
|
+
emitLatestBlocks(
|
|
186
|
+
value.blockNumber,
|
|
187
|
+
value.timestamp,
|
|
188
|
+
contentBlocksForProtocol,
|
|
173
189
|
);
|
|
190
|
+
for (const { event, payload } of blockAppEvents) {
|
|
191
|
+
EventManager.Instance.sendMessage(event, payload as any).catch((err) => {
|
|
192
|
+
log.local(
|
|
193
|
+
ComponentNames.EFFECTSTREAM_RUNTIME,
|
|
194
|
+
"event-publish",
|
|
195
|
+
SeverityNumber.WARN,
|
|
196
|
+
(l) =>
|
|
197
|
+
l(`publish ${event.path.join("/")} failed: ${String(err)}`),
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
174
201
|
|
|
175
202
|
const lagMs = Date.now() - value.timestamp;
|
|
176
203
|
const lagSuffix = lagThresholdMs != null && lagMs > lagThresholdMs
|
|
@@ -205,24 +232,40 @@ function getRangesForSyncProtocols(value: ChainBlock): Record<string, [number, n
|
|
|
205
232
|
return contentBlocksForProtocol;
|
|
206
233
|
}
|
|
207
234
|
|
|
208
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Publish built-in block-level events.
|
|
237
|
+
*
|
|
238
|
+
* Fire-and-forget for the same reasons as the app-event flush above
|
|
239
|
+
* (do not stall the sync loop; ordering preserved by Opifex + MQTT 3.1.1).
|
|
240
|
+
*
|
|
241
|
+
* Errors are caught locally so they don't escape to the global
|
|
242
|
+
* `unhandledrejection` handler without context.
|
|
243
|
+
*/
|
|
244
|
+
function emitLatestBlocks(
|
|
209
245
|
rollUpBlockHeight: number,
|
|
210
246
|
rollUpBlockTimestamp: number,
|
|
211
247
|
syncChains: Record<string, [number, number]>,
|
|
212
|
-
) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
248
|
+
): void {
|
|
249
|
+
const logFailure = (topic: string) => (err: unknown) =>
|
|
250
|
+
log.local(
|
|
251
|
+
ComponentNames.EFFECTSTREAM_RUNTIME,
|
|
252
|
+
"event-publish",
|
|
253
|
+
SeverityNumber.WARN,
|
|
254
|
+
(l) => l(`publish ${topic} failed: ${String(err)}`),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
EventManager.Instance.sendMessage(BuiltinEvents.RollupBlock, {
|
|
258
|
+
block: rollUpBlockHeight,
|
|
259
|
+
timestamp: rollUpBlockTimestamp,
|
|
260
|
+
}).catch(logFailure("RollupBlock"));
|
|
261
|
+
|
|
262
|
+
for (const [chainName, [_, toBlock]] of Object.entries(syncChains)) {
|
|
263
|
+
EventManager.Instance.sendMessage(BuiltinEvents.SyncChains, {
|
|
264
|
+
chain: chainName,
|
|
265
|
+
block: toBlock,
|
|
266
|
+
rollup: rollUpBlockHeight,
|
|
267
|
+
}).catch(logFailure(`SyncChains/${chainName}`));
|
|
268
|
+
}
|
|
226
269
|
}
|
|
227
270
|
|
|
228
271
|
function* startup(
|
package/src/process-blocks.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChainBlock } from "@effectstream/sync";
|
|
2
2
|
import { call, type Operation, until } from "effection";
|
|
3
3
|
import type { Pool } from "pg";
|
|
4
|
-
import { type BaseStfInput, primitiveTransitionFunction } from "@effectstream/sm";
|
|
4
|
+
import { type BaseStfInput, type EmitFn, primitiveTransitionFunction } from "@effectstream/sm";
|
|
5
5
|
import { PreparedQuery } from "@pgtyped/runtime";
|
|
6
6
|
import type {
|
|
7
7
|
ExecPromise,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "@effectstream/coroutine";
|
|
11
11
|
import {
|
|
12
12
|
blockHeightDone,
|
|
13
|
+
deleteEmptyBlocks,
|
|
13
14
|
deleteScheduled,
|
|
14
15
|
getFutureGameInputByBlockHeight,
|
|
15
16
|
getFutureGameInputByMaxTimestamp,
|
|
@@ -23,6 +24,24 @@ import type { StartConfig } from "./types.ts";
|
|
|
23
24
|
import type { BlockNumber, EffectstreamBlockHash } from "@effectstream/utils";
|
|
24
25
|
import { generateEffectstreamBlockHash, Prando } from "@effectstream/crypto";
|
|
25
26
|
import { applyUserMigrations } from "./version-migrations.ts";
|
|
27
|
+
import type { EventPathAndDef } from "@effectstream/event-client";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A pending app event collected during STF execution, flushed to MQTT by
|
|
31
|
+
* the caller (main.ts) only after the block-level `COMMIT` succeeds.
|
|
32
|
+
*
|
|
33
|
+
* See plan invariants I1 (post-COMMIT delivery) and I2 (rollback drops events)
|
|
34
|
+
* in /Users/edwardalvarado/.claude/plans/wild-kindling-reddy.md.
|
|
35
|
+
*/
|
|
36
|
+
export type PendingEvent = {
|
|
37
|
+
event: EventPathAndDef;
|
|
38
|
+
payload: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ProcessFinalizedBlockResult = {
|
|
42
|
+
blockHash: EffectstreamBlockHash;
|
|
43
|
+
events: PendingEvent[];
|
|
44
|
+
};
|
|
26
45
|
|
|
27
46
|
/** Helper to check if a SyncStateUpdateStream object is a WorldResolve */
|
|
28
47
|
function isWorldResolve(value: any): value is QueuedUpdate {
|
|
@@ -127,12 +146,17 @@ export function* processFinalizedBlock(
|
|
|
127
146
|
config: StartConfig,
|
|
128
147
|
dbConn: Pool,
|
|
129
148
|
previousBlockHash: EffectstreamBlockHash | null,
|
|
130
|
-
): Operation<
|
|
149
|
+
): Operation<ProcessFinalizedBlockResult> {
|
|
131
150
|
const { gameStateTransitions, migrations } = config;
|
|
132
151
|
const blockHash: EffectstreamBlockHash = generateEffectstreamBlockHash(
|
|
133
152
|
value,
|
|
134
153
|
previousBlockHash,
|
|
135
154
|
);
|
|
155
|
+
// Per-block event buffer. Only events from STF runs that succeeded
|
|
156
|
+
// (and survived to the post-COMMIT return) end up here. On block-level
|
|
157
|
+
// ROLLBACK below, this buffer goes out of scope unflushed.
|
|
158
|
+
const blockEvents: PendingEvent[] = [];
|
|
159
|
+
let hasOperations = false;
|
|
136
160
|
try {
|
|
137
161
|
/* STEP 0: Start the transaction. */
|
|
138
162
|
yield* until(dbConn.query("BEGIN"));
|
|
@@ -151,8 +175,9 @@ export function* processFinalizedBlock(
|
|
|
151
175
|
);
|
|
152
176
|
|
|
153
177
|
/* STEP 2: Process the migrations. */
|
|
178
|
+
let hasMigrations = false;
|
|
154
179
|
if (migrations) {
|
|
155
|
-
yield* applyUserMigrations(
|
|
180
|
+
hasMigrations = yield* applyUserMigrations(
|
|
156
181
|
value.blockNumber,
|
|
157
182
|
dbConn as any, // Client,
|
|
158
183
|
migrations,
|
|
@@ -190,6 +215,18 @@ export function* processFinalizedBlock(
|
|
|
190
215
|
randomGenerator.skip();
|
|
191
216
|
for (const data of scheduledData) {
|
|
192
217
|
let success = true;
|
|
218
|
+
// Per-input event buffer. If the STF throws below, this buffer is
|
|
219
|
+
// dropped (events from rolled-back inputs never reach MQTT — I2).
|
|
220
|
+
// On success, we promote into `blockEvents` after the catch block.
|
|
221
|
+
const inputEvents: PendingEvent[] = [];
|
|
222
|
+
const emit: EmitFn = (event, payload) => {
|
|
223
|
+
inputEvents.push({
|
|
224
|
+
event,
|
|
225
|
+
// Auto-inject blockHeight (I5). registerEvents prepends it as the
|
|
226
|
+
// first indexed field; apps never set it themselves.
|
|
227
|
+
payload: { ...payload, blockHeight: value.blockNumber },
|
|
228
|
+
});
|
|
229
|
+
};
|
|
193
230
|
try {
|
|
194
231
|
const input: BaseStfInput = {
|
|
195
232
|
blockTimestamp: value.timestamp,
|
|
@@ -198,6 +235,7 @@ export function* processFinalizedBlock(
|
|
|
198
235
|
signerAddress: data.from_address,
|
|
199
236
|
signerAddressType: data.from_address_type,
|
|
200
237
|
randomGenerator,
|
|
238
|
+
emit,
|
|
201
239
|
// TODO: We might want to add this to the scheduled data.
|
|
202
240
|
//
|
|
203
241
|
// Or do we resolve it here. Account might change.
|
|
@@ -210,6 +248,12 @@ export function* processFinalizedBlock(
|
|
|
210
248
|
input,
|
|
211
249
|
);
|
|
212
250
|
yield* executeGeneratorStepByStep(gameSTFGenerator, dbConn);
|
|
251
|
+
// STF succeeded: promote buffered events to the block buffer.
|
|
252
|
+
// Failure path falls through the catch below and `inputEvents`
|
|
253
|
+
// simply goes out of scope.
|
|
254
|
+
if (inputEvents.length > 0) {
|
|
255
|
+
blockEvents.push(...inputEvents);
|
|
256
|
+
}
|
|
213
257
|
} catch (err) {
|
|
214
258
|
success = false;
|
|
215
259
|
log.remote(
|
|
@@ -251,9 +295,15 @@ export function* processFinalizedBlock(
|
|
|
251
295
|
}
|
|
252
296
|
|
|
253
297
|
/* STEP 6: Mark the block as done. */
|
|
298
|
+
hasOperations = hasMigrations ||
|
|
299
|
+
value.primitives.length > 0 ||
|
|
300
|
+
scheduledData.length > 0;
|
|
301
|
+
if (!hasOperations) {
|
|
302
|
+
yield* call(() => deleteEmptyBlocks.run(undefined, dbConn));
|
|
303
|
+
}
|
|
254
304
|
yield* call(() =>
|
|
255
305
|
blockHeightDone.run({
|
|
256
|
-
block_hash: Buffer.from(blockHash),
|
|
306
|
+
block_hash: Buffer.from(hasOperations ? blockHash : "0x0"),
|
|
257
307
|
block_height: value.blockNumber,
|
|
258
308
|
}, dbConn)
|
|
259
309
|
);
|
|
@@ -298,5 +348,9 @@ export function* processFinalizedBlock(
|
|
|
298
348
|
// We cannot recover from this error.
|
|
299
349
|
throw err;
|
|
300
350
|
}
|
|
301
|
-
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
blockHash: hasOperations ? blockHash : "0x0" as PaimaBlockHash,
|
|
354
|
+
events: blockEvents
|
|
355
|
+
};
|
|
302
356
|
}
|
|
@@ -95,7 +95,7 @@ export function* applyUserMigrations(
|
|
|
95
95
|
currentBlockHeight: number,
|
|
96
96
|
dbConn: Client,
|
|
97
97
|
migrationOrder: DBMigrations[] | undefined,
|
|
98
|
-
): Operation<
|
|
98
|
+
): Operation<boolean> {
|
|
99
99
|
const migrations = yield* getAllUserMigrations(
|
|
100
100
|
EFFECTSTREAM_ENGINE_VERSION,
|
|
101
101
|
migrationOrder,
|
|
@@ -114,6 +114,7 @@ export function* applyUserMigrations(
|
|
|
114
114
|
),
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
|
+
return migrations.length > 0;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function* getAllUserMigrations(
|