@effectstream/runtime 0.100.19 → 0.100.22
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 +13 -13
- package/src/api/http-server.ts +7 -55
- package/src/main.ts +38 -32
- package/src/process-blocks.ts +27 -126
- package/src/types.ts +0 -7
- package/test/reproduction/harness.ts +270 -556
- package/test/reproduction/sanity.test.ts +91 -40
- package/conciseinputhash-determinism.md +0 -114
- package/src/api/apply-status.ts +0 -30
- package/test/reproduction/buffering.test.ts +0 -389
- package/test/reproduction/consistency-snapshot.ts +0 -168
- package/test/reproduction/consistency.test.ts +0 -197
- package/test/reproduction/node-runner.ts +0 -152
- package/test/reproduction/poll.ts +0 -160
- package/test/reproduction/resume.test.ts +0 -77
- package/test/reproduction/scenario.ts +0 -126
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resume-marker position over the synthetic `test` chain.
|
|
3
|
-
*
|
|
4
|
-
* Companion to consistency.test.ts (which proves a mid-chunk restart re-fetches
|
|
5
|
-
* uncommitted data instead of skipping it). This file pins down *where* the
|
|
6
|
-
* persisted resume marker lands for a chain whose data is sparse — the property
|
|
7
|
-
* that bounds restart re-scan (see @effectstream/sync CLAUDE.md, design idea #5).
|
|
8
|
-
*
|
|
9
|
-
* The marker is the highest datum merged into the committed block, and because
|
|
10
|
-
* each fetched chunk's boundary page is buffered and consumed even when empty, it
|
|
11
|
-
* tracks the **committed frontier** — NOT the last block that happened to carry
|
|
12
|
-
* data. So a chain that produced one event long ago and has been quiet since
|
|
13
|
-
* still resumes near the frontier, re-fetching at most ~stepSize blocks, never
|
|
14
|
-
* its whole quiet tail. Here: data lives only at block 5, yet the marker must sit
|
|
15
|
-
* at the frontier (height 50 / 60), not stuck back at 5.
|
|
16
|
-
*/
|
|
17
|
-
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
18
|
-
import {
|
|
19
|
-
countEventRows,
|
|
20
|
-
type Harness,
|
|
21
|
-
latestFinalizedHeight,
|
|
22
|
-
maxPage,
|
|
23
|
-
setupHarness,
|
|
24
|
-
} from "./harness.ts";
|
|
25
|
-
|
|
26
|
-
let h: Harness;
|
|
27
|
-
|
|
28
|
-
beforeAll(async () => {
|
|
29
|
-
h = await setupHarness();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterAll(async () => {
|
|
33
|
-
await h?.teardown();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test(
|
|
37
|
-
"a sparse chain's resume marker tracks the frontier, not its last data block",
|
|
38
|
-
async () => {
|
|
39
|
-
// Data lives ONLY at parallel block 5; everything after is quiet. Block 5 is
|
|
40
|
-
// the marker a naive "resume from last data" scheme would persist.
|
|
41
|
-
const events = [{ atBlock: 5, marker: "evt5" }];
|
|
42
|
-
|
|
43
|
-
// Phase 1 — sync the main clock to 50 (parallel tip stays ahead so the merge
|
|
44
|
-
// can finalize). The block-5 event is merged early; the chain is quiet after.
|
|
45
|
-
await h.runToHeight({
|
|
46
|
-
events,
|
|
47
|
-
tips: { mainClock: 50, parallelP: 200 },
|
|
48
|
-
target: 50,
|
|
49
|
-
apiPort: 19161,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
expect(await latestFinalizedHeight(h.pool)).toBe(50);
|
|
53
|
-
expect(await countEventRows(h.pool, "evt5")).toBe(1);
|
|
54
|
-
|
|
55
|
-
// The crux: the parallel marker tracks the committed frontier (50), NOT the
|
|
56
|
-
// last data block (5). A restart therefore resumes from ~51 and re-scans only
|
|
57
|
-
// the bounded gap up to the frontier — never blocks 6..50 from scratch.
|
|
58
|
-
expect(await maxPage(h.pool, "parallelP")).toBe(50);
|
|
59
|
-
expect(await maxPage(h.pool, "parallelP")).not.toBe(5);
|
|
60
|
-
|
|
61
|
-
// Phase 2 (restart) — boot a fresh process against the same DB and sync on to
|
|
62
|
-
// 60. The parallel chain resumes from the frontier (51), not from block 5.
|
|
63
|
-
await h.runToHeight({
|
|
64
|
-
events,
|
|
65
|
-
tips: { mainClock: 60, parallelP: 200 },
|
|
66
|
-
target: 60,
|
|
67
|
-
apiPort: 19162,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(await latestFinalizedHeight(h.pool)).toBe(60);
|
|
71
|
-
// Marker advanced with the new frontier...
|
|
72
|
-
expect(await maxPage(h.pool, "parallelP")).toBe(60);
|
|
73
|
-
// ...and block 5 was neither re-fetched (resume started past it) nor lost.
|
|
74
|
-
expect(await countEventRows(h.pool, "evt5")).toBe(1);
|
|
75
|
-
},
|
|
76
|
-
120_000,
|
|
77
|
-
);
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared scenario building blocks for the reproduction tests.
|
|
3
|
-
*
|
|
4
|
-
* Both the in-process test files and the out-of-process {@link ./node-runner.ts}
|
|
5
|
-
* subprocess build the *same* synthetic `test`-chain config from these helpers,
|
|
6
|
-
* so a node booted in a child process is identical to one a test would describe.
|
|
7
|
-
* Keeping the config + primitive here (no DB, no cluster imports) lets the
|
|
8
|
-
* subprocess import it cheaply.
|
|
9
|
-
*/
|
|
10
|
-
import {
|
|
11
|
-
ConfigBuilder,
|
|
12
|
-
ConfigNetworkType,
|
|
13
|
-
ConfigSyncProtocolType,
|
|
14
|
-
} from "@effectstream/config";
|
|
15
|
-
import { Primitive } from "@effectstream/sm";
|
|
16
|
-
|
|
17
|
-
/** Fixed wall-clock origin so block timestamps are pure arithmetic. */
|
|
18
|
-
export const START_TIME = 1_700_000_000_000;
|
|
19
|
-
export const BLOCK_TIME_MS = 1000;
|
|
20
|
-
|
|
21
|
-
export type EventSpec = { atBlock: number; marker: string };
|
|
22
|
-
|
|
23
|
-
export type ScenarioOpts = {
|
|
24
|
-
events: EventSpec[];
|
|
25
|
-
/** Parallel-chain page size. Large (default 1000) => whole tip is one chunk. */
|
|
26
|
-
parallelStepSize?: number;
|
|
27
|
-
/** Security namespace; distinct per test keeps databases independent. */
|
|
28
|
-
securityNamespace?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export function buildConfig(opts: ScenarioOpts) {
|
|
32
|
-
const stepSize = opts.parallelStepSize ?? 1000;
|
|
33
|
-
const namespace = opts.securityNamespace ?? "sync-repro";
|
|
34
|
-
return new ConfigBuilder()
|
|
35
|
-
.setNamespace((b) => b.setSecurityNamespace(namespace))
|
|
36
|
-
.buildNetworks((b) =>
|
|
37
|
-
b
|
|
38
|
-
.addNetwork({
|
|
39
|
-
name: "clock",
|
|
40
|
-
type: ConfigNetworkType.TEST,
|
|
41
|
-
startTime: START_TIME,
|
|
42
|
-
blockTimeMS: BLOCK_TIME_MS,
|
|
43
|
-
})
|
|
44
|
-
.addNetwork({
|
|
45
|
-
name: "chainP",
|
|
46
|
-
type: ConfigNetworkType.TEST,
|
|
47
|
-
startTime: START_TIME,
|
|
48
|
-
blockTimeMS: BLOCK_TIME_MS,
|
|
49
|
-
})
|
|
50
|
-
)
|
|
51
|
-
.buildDeployments((b) => b)
|
|
52
|
-
.buildSyncProtocols((b) =>
|
|
53
|
-
b
|
|
54
|
-
.addMain(
|
|
55
|
-
(n) => n.clock,
|
|
56
|
-
() => ({
|
|
57
|
-
name: "mainClock",
|
|
58
|
-
type: ConfigSyncProtocolType.TEST_MAIN,
|
|
59
|
-
startBlockHeight: 1,
|
|
60
|
-
pollingInterval: 30,
|
|
61
|
-
stepSize: 1000,
|
|
62
|
-
}),
|
|
63
|
-
)
|
|
64
|
-
.addParallel(
|
|
65
|
-
(n) => (n as any).chainP,
|
|
66
|
-
() => ({
|
|
67
|
-
name: "parallelP",
|
|
68
|
-
type: ConfigSyncProtocolType.TEST_PARALLEL,
|
|
69
|
-
startBlockHeight: 1,
|
|
70
|
-
pollingInterval: 30,
|
|
71
|
-
stepSize,
|
|
72
|
-
delayMs: 0,
|
|
73
|
-
events: opts.events.map((e) => ({
|
|
74
|
-
atBlock: e.atBlock,
|
|
75
|
-
payload: { marker: e.marker },
|
|
76
|
-
})),
|
|
77
|
-
}),
|
|
78
|
-
)
|
|
79
|
-
)
|
|
80
|
-
.buildPrimitives((b) =>
|
|
81
|
-
b.addPrimitive(
|
|
82
|
-
(sp) => (sp as any).parallelP,
|
|
83
|
-
() => ({
|
|
84
|
-
name: "testEvt",
|
|
85
|
-
type: TEST_PRIMITIVE_TYPE,
|
|
86
|
-
startBlockHeight: 1,
|
|
87
|
-
}),
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
.build();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── A minimal user-defined primitive ─────────────────────────────────────────
|
|
94
|
-
// Writes one row to effectstream.primitive_accounting per event (no STF needed:
|
|
95
|
-
// stateMachinePayload is null). Tests assert on that table.
|
|
96
|
-
export const TEST_PRIMITIVE_TYPE = "TEST:EVENT";
|
|
97
|
-
|
|
98
|
-
export class TestEventPrimitive extends Primitive<any, any> {
|
|
99
|
-
readonly internalTypeName = TEST_PRIMITIVE_TYPE as any;
|
|
100
|
-
override grammar = [] as any;
|
|
101
|
-
constructor(config: any) {
|
|
102
|
-
super(config);
|
|
103
|
-
}
|
|
104
|
-
// deno-lint-ignore require-yield
|
|
105
|
-
override *getPayload(_height: any, primitiveTransactionData: any): any {
|
|
106
|
-
const payload = primitiveTransactionData.output.payload;
|
|
107
|
-
return {
|
|
108
|
-
isBatched: false,
|
|
109
|
-
data: [
|
|
110
|
-
{
|
|
111
|
-
fromAddressAndType: { type: "none", address: "0x0" },
|
|
112
|
-
accountingPayload: payload,
|
|
113
|
-
stateMachinePayload: null,
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
override getConfig(): any {
|
|
119
|
-
return {
|
|
120
|
-
name: this.instanceName,
|
|
121
|
-
type: this.internalTypeName,
|
|
122
|
-
startBlockHeight: this.startBlockHeight,
|
|
123
|
-
scheduledPrefix: this.stateMachinePrefix,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|