@secondlayer/shared 7.6.0 → 7.7.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.
@@ -0,0 +1,49 @@
1
+ import { Writable } from "node:stream";
2
+ /**
3
+ * Streaming `COPY ... FROM STDIN` loader for canonical archive rows.
4
+ *
5
+ * Restoring history row-by-row is not viable at chain scale: batched INSERTs
6
+ * measured ~2k rows/s on this schema, which is days for a full genesis restore.
7
+ * COPY moves the same data at ~64k rows/s because it is one statement per
8
+ * partition with no per-row round-trip.
9
+ *
10
+ * This module deliberately knows nothing about Parquet or about where rows come
11
+ * from — it accepts the archive's row shape as an async iterable. That keeps the
12
+ * heavy file-format dependency out of every consumer that only needs to write
13
+ * rows, and lets the CLI and the indexer share one loader rather than growing
14
+ * two subtly different CSV encoders.
15
+ */
16
+ type ArchiveDataset = "blocks" | "transactions" | "events";
17
+ /** Row shapes exactly as the archive's Parquet columns are named. */
18
+ type ArchiveRow = Record<string, unknown>;
19
+ /**
20
+ * CSV encoding for `WITH (FORMAT csv, NULL '\N')`: every present value is
21
+ * quoted with embedded quotes doubled, so a value that happens to equal the
22
+ * NULL marker cannot be misread as SQL NULL. Only a genuinely absent value is
23
+ * written unquoted as `\N`.
24
+ */
25
+ declare function csvField(value: string | number | null | undefined): string;
26
+ declare function toCsvLine(dataset: ArchiveDataset, row: ArchiveRow): string;
27
+ /**
28
+ * `canonical` is omitted from the blocks column list: every archived block is
29
+ * canonical by definition, and the schema defaults it to true, so carrying it
30
+ * through every row would be dead weight.
31
+ */
32
+ declare function copyStatement(dataset: ArchiveDataset): string;
33
+ /**
34
+ * Stream rows onto an open COPY writable. The caller owns opening it (drivers
35
+ * differ in how a COPY stream is obtained) and this owns the wire format,
36
+ * backpressure, and failure semantics.
37
+ *
38
+ * On error the stream is destroyed rather than ended, so Postgres aborts the
39
+ * COPY instead of committing a partial one — an interrupted load must leave
40
+ * nothing behind, not half a partition.
41
+ */
42
+ declare function writeRowsToCopyStream(params: {
43
+ writable: Writable
44
+ dataset: ArchiveDataset
45
+ rows: AsyncIterable<ArchiveRow>
46
+ onProgress?: (rowsWritten: number) => void
47
+ progressEvery?: number
48
+ }): Promise<number>;
49
+ export { writeRowsToCopyStream, toCsvLine, csvField, copyStatement, ArchiveRow, ArchiveDataset };
@@ -0,0 +1,111 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/copy-loader.ts
18
+ import { once } from "node:events";
19
+ function csvField(value) {
20
+ if (value === null || value === undefined)
21
+ return "\\N";
22
+ const text = typeof value === "number" ? String(value) : value;
23
+ return `"${text.replace(/"/g, '""')}"`;
24
+ }
25
+ function asNumber(value) {
26
+ return typeof value === "bigint" ? Number(value) : Number(value);
27
+ }
28
+ function asText(value) {
29
+ return Buffer.isBuffer(value) ? value.toString("utf8") : String(value);
30
+ }
31
+ function asOptionalText(value) {
32
+ return value === null || value === undefined ? null : asText(value);
33
+ }
34
+ function toCsvLine(dataset, row) {
35
+ if (dataset === "blocks") {
36
+ return [
37
+ csvField(asNumber(row.height)),
38
+ csvField(asText(row.hash)),
39
+ csvField(asText(row.parent_hash)),
40
+ csvField(asNumber(row.burn_block_height)),
41
+ csvField(asOptionalText(row.burn_block_hash)),
42
+ csvField(asOptionalText(row.index_block_hash)),
43
+ csvField(asNumber(row.timestamp))
44
+ ].join(",");
45
+ }
46
+ if (dataset === "transactions") {
47
+ return [
48
+ csvField(asText(row.tx_id)),
49
+ csvField(asNumber(row.block_height)),
50
+ csvField(asNumber(row.tx_index)),
51
+ csvField(asText(row.type)),
52
+ csvField(asText(row.sender)),
53
+ csvField(asText(row.status)),
54
+ csvField(asOptionalText(row.contract_id)),
55
+ csvField(asOptionalText(row.function_name)),
56
+ csvField(asOptionalText(row.function_args_json)),
57
+ csvField(asOptionalText(row.raw_result)),
58
+ csvField(asText(row.raw_tx))
59
+ ].join(",");
60
+ }
61
+ return [
62
+ csvField(asText(row.tx_id)),
63
+ csvField(asNumber(row.block_height)),
64
+ csvField(asNumber(row.event_index)),
65
+ csvField(asText(row.event_type)),
66
+ csvField(asText(row.data_json))
67
+ ].join(",");
68
+ }
69
+ function copyStatement(dataset) {
70
+ if (dataset === "blocks") {
71
+ return "COPY blocks (height, hash, parent_hash, burn_block_height, burn_block_hash, index_block_hash, timestamp) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
72
+ }
73
+ if (dataset === "transactions") {
74
+ return "COPY transactions (tx_id, block_height, tx_index, type, sender, status, contract_id, function_name, function_args, raw_result, raw_tx) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
75
+ }
76
+ return "COPY events (tx_id, block_height, event_index, type, data) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
77
+ }
78
+ async function writeRowsToCopyStream(params) {
79
+ const { writable, dataset, rows } = params;
80
+ const progressEvery = params.progressEvery ?? 500000;
81
+ let count = 0;
82
+ try {
83
+ for await (const row of rows) {
84
+ const line = `${toCsvLine(dataset, row)}
85
+ `;
86
+ if (!writable.write(line)) {
87
+ await once(writable, "drain");
88
+ }
89
+ count++;
90
+ if (params.onProgress && count % progressEvery === 0) {
91
+ params.onProgress(count);
92
+ }
93
+ }
94
+ await new Promise((resolve, reject) => {
95
+ writable.end((error) => error ? reject(error) : resolve());
96
+ });
97
+ } catch (error) {
98
+ writable.destroy(error instanceof Error ? error : new Error(String(error)));
99
+ throw error;
100
+ }
101
+ return count;
102
+ }
103
+ export {
104
+ writeRowsToCopyStream,
105
+ toCsvLine,
106
+ csvField,
107
+ copyStatement
108
+ };
109
+
110
+ //# debugId=2B81DFB47D25D63C64756E2164756E21
111
+ //# sourceMappingURL=copy-loader.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/copy-loader.ts"],
4
+ "sourcesContent": [
5
+ "import { once } from \"node:events\";\nimport type { Writable } from \"node:stream\";\n\n/**\n * Streaming `COPY ... FROM STDIN` loader for canonical archive rows.\n *\n * Restoring history row-by-row is not viable at chain scale: batched INSERTs\n * measured ~2k rows/s on this schema, which is days for a full genesis restore.\n * COPY moves the same data at ~64k rows/s because it is one statement per\n * partition with no per-row round-trip.\n *\n * This module deliberately knows nothing about Parquet or about where rows come\n * from — it accepts the archive's row shape as an async iterable. That keeps the\n * heavy file-format dependency out of every consumer that only needs to write\n * rows, and lets the CLI and the indexer share one loader rather than growing\n * two subtly different CSV encoders.\n */\n\nexport type ArchiveDataset = \"blocks\" | \"transactions\" | \"events\";\n\n/** Row shapes exactly as the archive's Parquet columns are named. */\nexport type ArchiveRow = Record<string, unknown>;\n\n/**\n * CSV encoding for `WITH (FORMAT csv, NULL '\\N')`: every present value is\n * quoted with embedded quotes doubled, so a value that happens to equal the\n * NULL marker cannot be misread as SQL NULL. Only a genuinely absent value is\n * written unquoted as `\\N`.\n */\nexport function csvField(value: string | number | null | undefined): string {\n\tif (value === null || value === undefined) return \"\\\\N\";\n\tconst text = typeof value === \"number\" ? String(value) : value;\n\treturn `\"${text.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction asNumber(value: unknown): number {\n\treturn typeof value === \"bigint\" ? Number(value) : Number(value as number);\n}\n\nfunction asText(value: unknown): string {\n\treturn Buffer.isBuffer(value) ? value.toString(\"utf8\") : String(value);\n}\n\nfunction asOptionalText(value: unknown): string | null {\n\treturn value === null || value === undefined ? null : asText(value);\n}\n\nexport function toCsvLine(dataset: ArchiveDataset, row: ArchiveRow): string {\n\tif (dataset === \"blocks\") {\n\t\treturn [\n\t\t\tcsvField(asNumber(row.height)),\n\t\t\tcsvField(asText(row.hash)),\n\t\t\tcsvField(asText(row.parent_hash)),\n\t\t\tcsvField(asNumber(row.burn_block_height)),\n\t\t\tcsvField(asOptionalText(row.burn_block_hash)),\n\t\t\tcsvField(asOptionalText(row.index_block_hash)),\n\t\t\tcsvField(asNumber(row.timestamp)),\n\t\t].join(\",\");\n\t}\n\tif (dataset === \"transactions\") {\n\t\treturn [\n\t\t\tcsvField(asText(row.tx_id)),\n\t\t\tcsvField(asNumber(row.block_height)),\n\t\t\tcsvField(asNumber(row.tx_index)),\n\t\t\tcsvField(asText(row.type)),\n\t\t\tcsvField(asText(row.sender)),\n\t\t\tcsvField(asText(row.status)),\n\t\t\tcsvField(asOptionalText(row.contract_id)),\n\t\t\tcsvField(asOptionalText(row.function_name)),\n\t\t\tcsvField(asOptionalText(row.function_args_json)),\n\t\t\tcsvField(asOptionalText(row.raw_result)),\n\t\t\tcsvField(asText(row.raw_tx)),\n\t\t].join(\",\");\n\t}\n\treturn [\n\t\tcsvField(asText(row.tx_id)),\n\t\tcsvField(asNumber(row.block_height)),\n\t\tcsvField(asNumber(row.event_index)),\n\t\tcsvField(asText(row.event_type)),\n\t\tcsvField(asText(row.data_json)),\n\t].join(\",\");\n}\n\n/**\n * `canonical` is omitted from the blocks column list: every archived block is\n * canonical by definition, and the schema defaults it to true, so carrying it\n * through every row would be dead weight.\n */\nexport function copyStatement(dataset: ArchiveDataset): string {\n\tif (dataset === \"blocks\") {\n\t\treturn \"COPY blocks (height, hash, parent_hash, burn_block_height, burn_block_hash, index_block_hash, timestamp) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t}\n\tif (dataset === \"transactions\") {\n\t\treturn \"COPY transactions (tx_id, block_height, tx_index, type, sender, status, contract_id, function_name, function_args, raw_result, raw_tx) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t}\n\treturn \"COPY events (tx_id, block_height, event_index, type, data) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n}\n\n/**\n * Stream rows onto an open COPY writable. The caller owns opening it (drivers\n * differ in how a COPY stream is obtained) and this owns the wire format,\n * backpressure, and failure semantics.\n *\n * On error the stream is destroyed rather than ended, so Postgres aborts the\n * COPY instead of committing a partial one — an interrupted load must leave\n * nothing behind, not half a partition.\n */\nexport async function writeRowsToCopyStream(params: {\n\twritable: Writable;\n\tdataset: ArchiveDataset;\n\trows: AsyncIterable<ArchiveRow>;\n\tonProgress?: (rowsWritten: number) => void;\n\tprogressEvery?: number;\n}): Promise<number> {\n\tconst { writable, dataset, rows } = params;\n\tconst progressEvery = params.progressEvery ?? 500_000;\n\tlet count = 0;\n\ttry {\n\t\tfor await (const row of rows) {\n\t\t\tconst line = `${toCsvLine(dataset, row)}\\n`;\n\t\t\tif (!writable.write(line)) {\n\t\t\t\tawait once(writable, \"drain\");\n\t\t\t}\n\t\t\tcount++;\n\t\t\tif (params.onProgress && count % progressEvery === 0) {\n\t\t\t\tparams.onProgress(count);\n\t\t\t}\n\t\t}\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\twritable.end((error?: Error | null) =>\n\t\t\t\terror ? reject(error) : resolve(),\n\t\t\t);\n\t\t});\n\t} catch (error) {\n\t\twritable.destroy(error instanceof Error ? error : new Error(String(error)));\n\t\tthrow error;\n\t}\n\treturn count;\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "2B81DFB47D25D63C64756E2164756E21",
9
+ "names": []
10
+ }