@effectstream/runtime 0.100.13 → 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 +78 -38
- 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,6 +7,7 @@ import {
|
|
|
7
7
|
acquireDBMutex,
|
|
8
8
|
createDynamicTables,
|
|
9
9
|
getConnection,
|
|
10
|
+
getLastNonEmptyBlockHash,
|
|
10
11
|
releaseDBMutex,
|
|
11
12
|
resetPublicTables,
|
|
12
13
|
runSnapshotLoop,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
until,
|
|
29
30
|
} from "effection";
|
|
30
31
|
import { initTelemetry } from "./telemetry.ts";
|
|
31
|
-
import { processFinalizedBlock } from "./process-blocks.ts";
|
|
32
|
+
import { type PendingEvent, processFinalizedBlock } from "./process-blocks.ts";
|
|
32
33
|
import { startHttpServer } from "./api/http-server.ts";
|
|
33
34
|
import type { StartConfig } from "./types.ts";
|
|
34
35
|
import type { Client } from "pg";
|
|
@@ -40,19 +41,6 @@ import { builtInPrimitivesMap } from "@effectstream/sm";
|
|
|
40
41
|
import { validateAndSnapshotConfig } from "./config-snapshot.ts";
|
|
41
42
|
|
|
42
43
|
export function* init() {
|
|
43
|
-
// Prevent transient network errors (broken pipes, closed connections) from
|
|
44
|
-
// crashing the process via unhandled promise rejections. The sync loop's
|
|
45
|
-
// tryYield already retries on error — this just keeps the process alive.
|
|
46
|
-
globalThis.addEventListener("unhandledrejection", (event) => {
|
|
47
|
-
log.local(
|
|
48
|
-
ComponentNames.EFFECTSTREAM_RUNTIME,
|
|
49
|
-
"unhandled-rejection",
|
|
50
|
-
SeverityNumber.WARN,
|
|
51
|
-
(l) => l("Suppressed unhandled rejection:", event.reason),
|
|
52
|
-
);
|
|
53
|
-
event.preventDefault();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
44
|
// initialize OpenTelemetry
|
|
57
45
|
yield* initTelemetry();
|
|
58
46
|
}
|
|
@@ -137,7 +125,10 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
137
125
|
}
|
|
138
126
|
});
|
|
139
127
|
|
|
140
|
-
|
|
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;
|
|
141
132
|
if (config.snapshotConfig) {
|
|
142
133
|
yield* spawn(() => runSnapshotLoop(config.snapshotConfig!));
|
|
143
134
|
}
|
|
@@ -147,16 +138,25 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
147
138
|
// For PGLite, this is not enough, as the can only be one connection at a time.
|
|
148
139
|
// So we request a DBMutex as well.
|
|
149
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[] = [];
|
|
150
145
|
try {
|
|
151
146
|
yield* acquireDBMutex(`processing-blocks:${value.blockNumber}`);
|
|
152
147
|
dbClient = yield* until((dbConn as any).connect()); // Client,
|
|
153
148
|
|
|
154
|
-
|
|
149
|
+
const result = yield* processFinalizedBlock(
|
|
155
150
|
value,
|
|
156
151
|
config,
|
|
157
152
|
dbClient as any, // Client,
|
|
158
153
|
blockHash,
|
|
159
154
|
);
|
|
155
|
+
const resultHash = result.blockHash;
|
|
156
|
+
blockAppEvents = result.events;
|
|
157
|
+
if (resultHash !== "0x0") {
|
|
158
|
+
blockHash = resultHash;
|
|
159
|
+
}
|
|
160
160
|
} finally {
|
|
161
161
|
releaseDBMutex(`processing-blocks:${value.blockNumber}`);
|
|
162
162
|
if (dbClient) {
|
|
@@ -166,14 +166,38 @@ export function* start(config: StartConfig): Operation<void> {
|
|
|
166
166
|
|
|
167
167
|
// Used to emit & log the block range for each protocol.
|
|
168
168
|
const contentBlocksForProtocol = getRangesForSyncProtocols(value);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,
|
|
176
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
|
+
}
|
|
177
201
|
|
|
178
202
|
const lagMs = Date.now() - value.timestamp;
|
|
179
203
|
const lagSuffix = lagThresholdMs != null && lagMs > lagThresholdMs
|
|
@@ -208,24 +232,40 @@ function getRangesForSyncProtocols(value: ChainBlock): Record<string, [number, n
|
|
|
208
232
|
return contentBlocksForProtocol;
|
|
209
233
|
}
|
|
210
234
|
|
|
211
|
-
|
|
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(
|
|
212
245
|
rollUpBlockHeight: number,
|
|
213
246
|
rollUpBlockTimestamp: number,
|
|
214
247
|
syncChains: Record<string, [number, number]>,
|
|
215
|
-
) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
}
|
|
229
269
|
}
|
|
230
270
|
|
|
231
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(
|