@effectstream/runtime 0.100.26 → 0.100.27

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/README.md CHANGED
@@ -84,11 +84,21 @@ default:
84
84
  EFFECTSTREAM_COALESCE_EMPTY_BLOCKS=true
85
85
  ```
86
86
 
87
+ The lag threshold that defines "behind the chain tip" defaults to **20× the
88
+ main clock's block time** (falling back to 60 s when no `blockTimeMS` is
89
+ exposed by the network config). Override it with
90
+ `EFFECTSTREAM_LAG_THRESHOLD_MS` (milliseconds), e.g. for chains whose main
91
+ clock fires faster than the runtime can keep up with one transaction per block:
92
+
93
+ ```bash
94
+ EFFECTSTREAM_LAG_THRESHOLD_MS=30000 # engage coalescing past 30 s of lag
95
+ ```
96
+
87
97
  Behavior and guarantees:
88
98
 
89
99
  - **Catch-up only.** Coalescing engages only while behind the chain tip
90
- (`now - block.timestamp` greater than 10× the main clock's block time) and
91
- disengages at the tip, so steady-state stays one block per transaction.
100
+ (`now - block.timestamp` greater than the lag threshold) and disengages at
101
+ the tip, so steady-state stays one block per transaction.
92
102
  - **Identical database.** A coalesced sync produces a byte-for-byte identical
93
103
  database to a non-coalesced one. Empty blocks already never advance the block
94
104
  hash or RNG seed, so only the endpoint's block row and the per-protocol resume
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/runtime",
3
- "version": "0.100.26",
3
+ "version": "0.100.27",
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.26",
17
- "@effectstream/config": "0.100.26",
18
- "@effectstream/coroutine": "0.100.26",
19
- "@effectstream/crypto": "0.100.26",
20
- "@effectstream/db": "0.100.26",
21
- "@effectstream/event-client": "0.100.26",
22
- "@effectstream/event-server": "0.100.26",
23
- "@effectstream/log": "0.100.26",
24
- "@effectstream/sm": "0.100.26",
25
- "@effectstream/sync": "0.100.26",
26
- "@effectstream/utils": "0.100.26",
16
+ "@effectstream/concise": "0.100.27",
17
+ "@effectstream/config": "0.100.27",
18
+ "@effectstream/coroutine": "0.100.27",
19
+ "@effectstream/crypto": "0.100.27",
20
+ "@effectstream/db": "0.100.27",
21
+ "@effectstream/event-client": "0.100.27",
22
+ "@effectstream/event-server": "0.100.27",
23
+ "@effectstream/log": "0.100.27",
24
+ "@effectstream/sm": "0.100.27",
25
+ "@effectstream/sync": "0.100.27",
26
+ "@effectstream/utils": "0.100.27",
27
27
  "@fastify/cors": "^11.0.1",
28
28
  "@metamask/json-rpc-engine": "^10.0.3",
29
29
  "pg": "8.21.0",
package/src/coalesce.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Empty-block coalescing during deep catch-up: fold consecutive no-op blocks into one transaction. */
2
2
  import type { ChainBlock } from "@effectstream/sync";
3
3
  import { call, type Operation, race, sleep, until } from "effection";
4
- import type { Pool, PoolClient } from "pg";
4
+ import type { Client, Pool, PoolClient } from "pg";
5
5
  import {
6
6
  acquireDBMutex,
7
7
  blockHeightDone,
@@ -21,6 +21,7 @@ import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
21
21
  import { generateEffectstreamBlockHash, Prando } from "@effectstream/crypto";
22
22
  import type { EffectstreamBlockHash } from "@effectstream/utils";
23
23
  import type { DBMigrations } from "@effectstream/runtime";
24
+ import { mergeCoalescingBoundaries } from "@effectstream/sync";
24
25
 
25
26
  /** Scheduled-input boundaries sampled once at run start (stable until the next non-empty block). */
26
27
  export type EmptyRunBoundaries = {
@@ -44,14 +45,53 @@ export function* loadEmptyRunBoundaries(
44
45
  const minScheduledBlockHeight = bh?.min_block_height != null && bh.min_block_height >= fromBlockNumber
45
46
  ? bh.min_block_height
46
47
  : null;
48
+ const minScheduledTimestampMs = ts?.min_timestamp != null
49
+ ? new Date(ts.min_timestamp).getTime()
50
+ : null;
51
+
52
+ mergeCoalescingBoundaries.minScheduledBlockHeight = minScheduledBlockHeight;
53
+ mergeCoalescingBoundaries.minScheduledTimestampMs = minScheduledTimestampMs;
54
+
47
55
  return {
48
56
  minScheduledBlockHeight,
49
- minScheduledTimestampMs: ts?.min_timestamp != null
50
- ? new Date(ts.min_timestamp).getTime()
51
- : null,
57
+ minScheduledTimestampMs,
52
58
  };
53
59
  }
54
60
 
61
+ /**
62
+ * Seed {@link mergeCoalescingBoundaries} once at startup so the merge loop knows
63
+ * which empty blocks it may coalesce before the runtime has applied any block.
64
+ * `loadEmptyRunBoundaries` keeps these in sync afterwards on each non-empty block.
65
+ *
66
+ * @param fromBlockNumber the next block height to apply (lastBlockHeight + 1);
67
+ * scheduled heights below this are stale and ignored (see {@link loadEmptyRunBoundaries}).
68
+ */
69
+ export function* initMergeCoalescingBoundaries(
70
+ dbConn: Pool | Client,
71
+ fromBlockNumber: number,
72
+ migrations: DBMigrations[] | undefined,
73
+ ): Operation<void> {
74
+ const [bh] = yield* until(getEarliestScheduledBlockHeight.run(undefined, dbConn));
75
+ const [ts] = yield* until(getEarliestScheduledTimestamp.run(undefined, dbConn));
76
+ const minScheduledBlockHeight = bh?.min_block_height != null && bh.min_block_height >= fromBlockNumber
77
+ ? bh.min_block_height
78
+ : null;
79
+ const minScheduledTimestampMs = ts?.min_timestamp != null
80
+ ? new Date(ts.min_timestamp).getTime()
81
+ : null;
82
+
83
+ const migrationBlockHeights: number[] = [];
84
+ for (const m of migrations ?? []) {
85
+ if (m.blockHeight != null) {
86
+ migrationBlockHeights.push(m.blockHeight);
87
+ }
88
+ }
89
+
90
+ mergeCoalescingBoundaries.minScheduledBlockHeight = minScheduledBlockHeight;
91
+ mergeCoalescingBoundaries.minScheduledTimestampMs = minScheduledTimestampMs;
92
+ mergeCoalescingBoundaries.migrationBlockHeights = migrationBlockHeights;
93
+ }
94
+
55
95
  /** Mirrors the empty-block checks in process-blocks.ts without DB/STF work. */
56
96
  export function isCoalescableEmptyBlock(
57
97
  value: ChainBlock,
@@ -20,6 +20,7 @@ import {
20
20
  finalizedStreamStatus,
21
21
  recordConsumed,
22
22
  recordProduced,
23
+ recordCoalesced,
23
24
  } from "./api/stream-status.ts";
24
25
  import type { FinalizedBlockSubscription } from "./coalesce.ts";
25
26
 
@@ -52,6 +53,9 @@ export function* createBoundedFinalizedStream(
52
53
  yield* drained.wait();
53
54
  }
54
55
  recordProduced();
56
+ if (block.coalescedCount && block.coalescedCount > 1) {
57
+ recordCoalesced(block.coalescedCount - 1);
58
+ }
55
59
  yield* channel.send(block);
56
60
  },
57
61
  };
package/src/main.ts CHANGED
@@ -33,7 +33,10 @@ import {
33
33
  type PendingEvent,
34
34
  processFinalizedBlockWithRetry,
35
35
  } from "./process-blocks.ts";
36
- import { createEmptyBlockCoalescer } from "./coalesce.ts";
36
+ import {
37
+ createEmptyBlockCoalescer,
38
+ initMergeCoalescingBoundaries,
39
+ } from "./coalesce.ts";
37
40
  import { startHttpServer } from "./api/http-server.ts";
38
41
  import { recordAppliedBlock } from "./api/apply-status.ts";
39
42
  import { recordCoalesced } from "./api/stream-status.ts";
@@ -96,14 +99,15 @@ export function* start(config: StartConfig): Operation<void> {
96
99
  );
97
100
  });
98
101
 
99
- // 10× main clock block time (NTP if present, else protocol 0).
102
+ // 20× main clock block time (NTP if present, else protocol 0).
100
103
  // Falls back to 60 s so coalescing is not silently disabled for chains that
101
104
  // don't expose a blockTimeMS on their network config (e.g. Midnight-as-main).
102
105
  const ntpConfig = syncInfo.find(s => s.networkType === ConfigNetworkType.NTP);
103
106
  const clockBlockTimeMS =
104
107
  (ntpConfig?.network as { blockTimeMS?: number } | undefined)?.blockTimeMS ??
105
108
  (syncInfo[0]?.network as { blockTimeMS?: number } | undefined)?.blockTimeMS;
106
- const lagThresholdMs = clockBlockTimeMS != null ? clockBlockTimeMS * 10 : 60_000;
109
+ const lagThresholdMs = ENV.EFFECTSTREAM_LAG_THRESHOLD_MS ??
110
+ (clockBlockTimeMS != null ? clockBlockTimeMS * 20 : 60_000);
107
111
 
108
112
  // Bounded hand-off queue between the merge and the apply loop. Backpressure caps
109
113
  // the in-memory queue so deep catch-up can't grow it toward the whole backlog
@@ -355,6 +359,9 @@ function* startup(
355
359
  viewStrategy,
356
360
  );
357
361
 
362
+ // Seed the merge loop's coalescing boundaries before any block is produced.
363
+ yield* initMergeCoalescingBoundaries(dbConn, lastBlockHeight + 1, config.migrations);
364
+
358
365
  return syncProtocols;
359
366
  } finally {
360
367
  releaseDBMutex(`startup-node`);