@effectstream/runtime 0.100.28 → 0.101.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/package.json +12 -12
- package/src/config-snapshot.ts +19 -14
- package/src/process-blocks.ts +1 -0
- package/test/config-snapshot.unit.test.ts +99 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.101.0",
|
|
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.
|
|
17
|
-
"@effectstream/config": "0.
|
|
18
|
-
"@effectstream/coroutine": "0.
|
|
19
|
-
"@effectstream/crypto": "0.
|
|
20
|
-
"@effectstream/db": "0.
|
|
21
|
-
"@effectstream/event-client": "0.
|
|
22
|
-
"@effectstream/event-server": "0.
|
|
23
|
-
"@effectstream/log": "0.
|
|
24
|
-
"@effectstream/sm": "0.
|
|
25
|
-
"@effectstream/sync": "0.
|
|
26
|
-
"@effectstream/utils": "0.
|
|
16
|
+
"@effectstream/concise": "0.101.0",
|
|
17
|
+
"@effectstream/config": "0.101.0",
|
|
18
|
+
"@effectstream/coroutine": "0.101.0",
|
|
19
|
+
"@effectstream/crypto": "0.101.0",
|
|
20
|
+
"@effectstream/db": "0.101.0",
|
|
21
|
+
"@effectstream/event-client": "0.101.0",
|
|
22
|
+
"@effectstream/event-server": "0.101.0",
|
|
23
|
+
"@effectstream/log": "0.101.0",
|
|
24
|
+
"@effectstream/sm": "0.101.0",
|
|
25
|
+
"@effectstream/sync": "0.101.0",
|
|
26
|
+
"@effectstream/utils": "0.101.0",
|
|
27
27
|
"@fastify/cors": "^11.0.1",
|
|
28
28
|
"@metamask/json-rpc-engine": "^10.0.3",
|
|
29
29
|
"pg": "8.21.0",
|
package/src/config-snapshot.ts
CHANGED
|
@@ -70,6 +70,23 @@ function extractImmutableConfig(protocol: SyncProtocolWithNetwork): Record<strin
|
|
|
70
70
|
return { startBlockHeight: sp.startBlockHeight };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Compares a saved snapshot value against the current in-memory value.
|
|
75
|
+
* Falls back to structural JSON comparison for nested objects (which are
|
|
76
|
+
* always freshly constructed plain objects with stable key-insertion order
|
|
77
|
+
* from `extractImmutableConfig`), and to reference/identity comparison for
|
|
78
|
+
* everything else.
|
|
79
|
+
*/
|
|
80
|
+
function valuesDiffer(saved: unknown, current: unknown): boolean {
|
|
81
|
+
if (
|
|
82
|
+
typeof saved === "object" && saved !== null &&
|
|
83
|
+
typeof current === "object" && current !== null
|
|
84
|
+
) {
|
|
85
|
+
return JSON.stringify(saved) !== JSON.stringify(current);
|
|
86
|
+
}
|
|
87
|
+
return saved !== current;
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
/**
|
|
74
91
|
* Checks the saved config snapshot for each sync protocol against the current config.
|
|
75
92
|
*
|
|
@@ -85,7 +102,6 @@ export function* validateAndSnapshotConfig(
|
|
|
85
102
|
syncInfo: SyncProtocolWithNetwork[],
|
|
86
103
|
dbConn: Client,
|
|
87
104
|
): Operation<void> {
|
|
88
|
-
console.log("validateAndSnapshotConfig");
|
|
89
105
|
const useDbStartHeight = getEnv("USE_DB_STARTHEIGHT") !== undefined;
|
|
90
106
|
|
|
91
107
|
for (const protocol of syncInfo) {
|
|
@@ -119,20 +135,9 @@ export function* validateAndSnapshotConfig(
|
|
|
119
135
|
|
|
120
136
|
const saved = rows[0].immutable_config as Record<string, unknown>;
|
|
121
137
|
const mismatches: string[] = [];
|
|
122
|
-
console.log("saved", saved);
|
|
123
138
|
for (const [key, savedValue] of Object.entries(saved)) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// mismatches.push(` ${key}: saved=${JSON.stringify(savedValue)}, current=${JSON.stringify(currentImmutable[key])}`);
|
|
127
|
-
// }
|
|
128
|
-
if (typeof savedValue === "object" && savedValue !== null) {
|
|
129
|
-
for (const [k, v] of Object.entries(savedValue)) {
|
|
130
|
-
if ((currentImmutable[key] as any)[k] !== v) {
|
|
131
|
-
mismatches.push(` ${key}.${k}: saved=${JSON.stringify(v)},\n current=${JSON.stringify((currentImmutable[key] as any)[k])}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} else if (currentImmutable[key] !== savedValue) {
|
|
135
|
-
mismatches.push(` ${key}: saved=${JSON.stringify(savedValue)},\n current=${JSON.stringify(currentImmutable[key])}`);
|
|
139
|
+
if (valuesDiffer(savedValue, currentImmutable[key])) {
|
|
140
|
+
mismatches.push(` ${key}: saved=${JSON.stringify(savedValue)}, current=${JSON.stringify(currentImmutable[key])}`);
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
|
package/src/process-blocks.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { expect, test, mock, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { run } from "effection";
|
|
3
|
+
import type { Client } from "pg";
|
|
4
|
+
import { ConfigNetworkType, ConfigSyncProtocolType } from "@effectstream/config";
|
|
5
|
+
import type { SyncProtocolWithNetwork } from "@effectstream/config";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* In-memory backing store for the mocked `@effectstream/db` query objects.
|
|
9
|
+
* `validateAndSnapshotConfig` only touches two queries — `get…Snapshot`
|
|
10
|
+
* (returns rows) and `upsert…Snapshot` (writes) — so stubbing those two is
|
|
11
|
+
* sufficient to exercise the comparison loop end-to-end without a real DB.
|
|
12
|
+
*/
|
|
13
|
+
type StoredRow = { networkType: ConfigNetworkType; immutable_config: unknown };
|
|
14
|
+
let store: Record<string, StoredRow | undefined> = {};
|
|
15
|
+
|
|
16
|
+
mock.module("@effectstream/db", () => ({
|
|
17
|
+
getSyncProtocolConfigSnapshot: {
|
|
18
|
+
run: ({ protocolName }: { protocolName: string }) =>
|
|
19
|
+
Promise.resolve(store[protocolName] ? [store[protocolName]] : []),
|
|
20
|
+
},
|
|
21
|
+
upsertSyncProtocolConfigSnapshot: {
|
|
22
|
+
run: ({
|
|
23
|
+
protocolName,
|
|
24
|
+
networkType,
|
|
25
|
+
immutableConfig,
|
|
26
|
+
}: {
|
|
27
|
+
protocolName: string;
|
|
28
|
+
networkType: ConfigNetworkType;
|
|
29
|
+
immutableConfig: string;
|
|
30
|
+
}) => {
|
|
31
|
+
store[protocolName] = { networkType, immutable_config: JSON.parse(immutableConfig) };
|
|
32
|
+
return Promise.resolve([]);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const { validateAndSnapshotConfig } = await import("../src/config-snapshot.ts");
|
|
38
|
+
|
|
39
|
+
const STUB_DB = {} as Client;
|
|
40
|
+
|
|
41
|
+
function cardanoUtxoRpcProtocol(
|
|
42
|
+
startChainPoint: unknown,
|
|
43
|
+
): SyncProtocolWithNetwork {
|
|
44
|
+
return {
|
|
45
|
+
networkType: ConfigNetworkType.CARDANO,
|
|
46
|
+
syncProtocolType: ConfigSyncProtocolType.CARDANO_UTXORPC_PARALLEL,
|
|
47
|
+
syncProtocol: { name: "cardano-utxorpc", startChainPoint },
|
|
48
|
+
network: {},
|
|
49
|
+
} as unknown as SyncProtocolWithNetwork;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runValidate(
|
|
53
|
+
protocols: SyncProtocolWithNetwork[],
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
await run(function* () {
|
|
56
|
+
yield* validateAndSnapshotConfig(protocols, STUB_DB);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
store = {};
|
|
62
|
+
delete process.env.USE_DB_STARTHEIGHT;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
delete process.env.USE_DB_STARTHEIGHT;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("first run persists the snapshot and succeeds", async () => {
|
|
70
|
+
const protocol = cardanoUtxoRpcProtocol({ slot: 1, hash: "0xabc" });
|
|
71
|
+
await expect(runValidate([protocol])).resolves.toBeUndefined();
|
|
72
|
+
expect(store["cardano-utxorpc"]?.immutable_config).toEqual({
|
|
73
|
+
startChainPoint: { slot: 1, hash: "0xabc" },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("matching nested-object snapshot succeeds on subsequent runs", async () => {
|
|
78
|
+
// Seed the snapshot from a first run.
|
|
79
|
+
await runValidate([cardanoUtxoRpcProtocol({ slot: 5, hash: "0xdead" })]);
|
|
80
|
+
|
|
81
|
+
// Same shape on restart — must not throw.
|
|
82
|
+
await expect(
|
|
83
|
+
runValidate([cardanoUtxoRpcProtocol({ slot: 5, hash: "0xdead" })]),
|
|
84
|
+
).resolves.toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("reports a clean mismatch (no TypeError) when a saved nested object has no current counterpart", async () => {
|
|
88
|
+
// Snapshot persisted with a nested startChainPoint object.
|
|
89
|
+
await runValidate([cardanoUtxoRpcProtocol({ slot: 42, hash: "0xbeef" })]);
|
|
90
|
+
|
|
91
|
+
// On restart the current config has lost that field (shape drift / undefined).
|
|
92
|
+
const drifted = cardanoUtxoRpcProtocol(undefined);
|
|
93
|
+
|
|
94
|
+
// The robust comparator must surface this as a clean CRITICAL mismatch
|
|
95
|
+
// rather than throwing a `TypeError: Cannot read properties of undefined`.
|
|
96
|
+
await expect(runValidate([drifted])).rejects.toThrow(
|
|
97
|
+
/\[config-snapshot\] CRITICAL/,
|
|
98
|
+
);
|
|
99
|
+
});
|