@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/runtime",
3
- "version": "0.100.28",
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.100.28",
17
- "@effectstream/config": "0.100.28",
18
- "@effectstream/coroutine": "0.100.28",
19
- "@effectstream/crypto": "0.100.28",
20
- "@effectstream/db": "0.100.28",
21
- "@effectstream/event-client": "0.100.28",
22
- "@effectstream/event-server": "0.100.28",
23
- "@effectstream/log": "0.100.28",
24
- "@effectstream/sm": "0.100.28",
25
- "@effectstream/sync": "0.100.28",
26
- "@effectstream/utils": "0.100.28",
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",
@@ -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
- console.log("key", key, savedValue, currentImmutable[key]);
125
- // if (currentImmutable[key] !== savedValue) {
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
 
@@ -260,6 +260,7 @@ export function* processFinalizedBlock(
260
260
  blockEvents.push(...inputEvents);
261
261
  }
262
262
  } catch (err) {
263
+ if (isTransientPgError(err)) throw err;
263
264
  success = false;
264
265
  log.remote(
265
266
  ComponentNames.EFFECTSTREAM_SYNC,
@@ -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
+ });