gangtise-openapi-cli 0.37.1 → 0.38.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/README.md +51 -40
- package/dist/src/cli.js +84 -26
- package/dist/src/core/args.js +4 -7
- package/dist/src/core/client.js +78 -50
- package/dist/src/core/download.js +40 -6
- package/dist/src/core/endpoints.js +7 -0
- package/dist/src/core/errors.js +8 -0
- package/dist/src/core/normalize.js +35 -0
- package/dist/src/core/output.js +13 -10
- package/dist/src/core/perSecurity.js +87 -0
- package/dist/src/core/printer.js +110 -11
- package/dist/src/core/quoteSharding.js +98 -19
- package/dist/src/core/rowSink.js +196 -0
- package/dist/src/core/transport.js +51 -0
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +57 -158
- package/gangtise-openapi/references/commands/ai.md +6 -4
- package/gangtise-openapi/references/commands/fundamental.md +7 -6
- package/gangtise-openapi/references/commands/indicator.md +56 -73
- package/gangtise-openapi/references/commands/insight.md +11 -11
- package/gangtise-openapi/references/commands/quote.md +45 -19
- package/gangtise-openapi/references/commands/reference-and-lookup.md +5 -5
- package/gangtise-openapi/references/commands/tool.md +2 -2
- package/gangtise-openapi/references/commands/vault.md +3 -3
- package/gangtise-openapi/references/errors.md +129 -0
- package/gangtise-openapi/references/examples.md +14 -11
- package/gangtise-openapi/references/fields.md +18 -19
- package/gangtise-openapi/references/lookup-ids.md +3 -3
- package/gangtise-openapi/references/response-schema.md +9 -9
- package/package.json +3 -2
package/dist/src/core/printer.js
CHANGED
|
@@ -1,17 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { normalizeRows } from "./normalize.js";
|
|
2
|
-
import { pickList, renderOutput, saveOutputIfNeeded, streamOutputToFile } from "./output.js";
|
|
4
|
+
import { countOutputRows, pickList, renderOutput, saveOutputIfNeeded, streamOutputToFile } from "./output.js";
|
|
5
|
+
import { getRowSink } from "./rowSink.js";
|
|
3
6
|
import { extractTitles, writeTitleCache } from "./titleCache.js";
|
|
7
|
+
import { CLI_VERSION } from "../version.js";
|
|
4
8
|
const LARGE_RESULT_ROWS = 50_000;
|
|
5
9
|
function warnIfLargeInMemory(items, format) {
|
|
6
10
|
if (items && items.length >= LARGE_RESULT_ROWS) {
|
|
7
11
|
process.stderr.write(`[gangtise] note: ${items.length} rows in '${format}' is built entirely in memory; stream large exports to a file with --format jsonl --output <path> (or csv).\n`);
|
|
8
12
|
}
|
|
9
13
|
}
|
|
14
|
+
function localIso(d) {
|
|
15
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
16
|
+
const offset = -d.getTimezoneOffset();
|
|
17
|
+
const sign = offset >= 0 ? "+" : "-";
|
|
18
|
+
const abs = Math.abs(offset);
|
|
19
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
20
|
+
}
|
|
21
|
+
const SECRET_KEY = /^(access|secret|api|private)?key$|secret|token|password|credential/i;
|
|
22
|
+
function redactSecrets(value) {
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return value.map(redactSecrets);
|
|
25
|
+
if (value && typeof value === "object") {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const [key, inner] of Object.entries(value))
|
|
28
|
+
out[key] = SECRET_KEY.test(key) ? "[redacted]" : redactSecrets(inner);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
const eq = value.startsWith("--") ? value.indexOf("=") : -1;
|
|
33
|
+
const head = eq > 0 ? value.slice(0, eq + 1) : "";
|
|
34
|
+
const raw = eq > 0 ? value.slice(eq + 1) : value;
|
|
35
|
+
const trimmed = raw.trimStart();
|
|
36
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(trimmed);
|
|
39
|
+
if (parsed && typeof parsed === "object")
|
|
40
|
+
return head + JSON.stringify(redactSecrets(parsed));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
async function stageExportMeta(output, format, rows, columns, normalized, complete) {
|
|
49
|
+
const result = normalized && typeof normalized === "object" && !Array.isArray(normalized) ? { ...normalized } : {};
|
|
50
|
+
delete result.list;
|
|
51
|
+
const doc = {
|
|
52
|
+
file: path.basename(output),
|
|
53
|
+
format,
|
|
54
|
+
rows,
|
|
55
|
+
complete,
|
|
56
|
+
exitCode: complete ? 0 : 3,
|
|
57
|
+
command: redactSecrets(process.argv.slice(2)),
|
|
58
|
+
cliVersion: CLI_VERSION,
|
|
59
|
+
fetchedAt: localIso(new Date()),
|
|
60
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
61
|
+
columns,
|
|
62
|
+
result: redactSecrets(result),
|
|
63
|
+
};
|
|
64
|
+
const metaPath = `${output}.meta.json`;
|
|
65
|
+
const partPath = `${metaPath}.part`;
|
|
66
|
+
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
67
|
+
try {
|
|
68
|
+
await fs.writeFile(partPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
await fs.unlink(partPath).catch(() => { });
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
async commit() {
|
|
76
|
+
try {
|
|
77
|
+
await fs.rename(partPath, metaPath);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await fs.unlink(partPath).catch(() => { });
|
|
81
|
+
await fs.unlink(metaPath).catch(() => { });
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async discard() {
|
|
86
|
+
await fs.unlink(partPath).catch(() => { });
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
10
90
|
export async function printData(data, format, output, cache) {
|
|
91
|
+
const sink = getRowSink(data);
|
|
92
|
+
if (sink && !sink.opened)
|
|
93
|
+
data.list = sink.takeBuffer();
|
|
94
|
+
const streamed = sink?.opened ? sink : undefined;
|
|
95
|
+
const columns = data && typeof data === "object" && Array.isArray(data.fieldList)
|
|
96
|
+
? data.fieldList
|
|
97
|
+
: undefined;
|
|
11
98
|
const normalized = normalizeRows(data);
|
|
12
99
|
const items = pickList(normalized);
|
|
13
|
-
|
|
14
|
-
|
|
100
|
+
const showing = streamed ? streamed.rows : (items?.length ?? 0);
|
|
101
|
+
if (cache) {
|
|
102
|
+
const titles = { ...(streamed?.titles ?? {}), ...(items ? extractTitles(items, cache) : {}) };
|
|
15
103
|
if (Object.keys(titles).length > 0) {
|
|
16
104
|
writeTitleCache(cache.endpointKey, titles).catch(() => { });
|
|
17
105
|
}
|
|
@@ -19,21 +107,32 @@ export async function printData(data, format, output, cache) {
|
|
|
19
107
|
if (normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
|
|
20
108
|
const meta = normalized;
|
|
21
109
|
if (typeof meta.total === "number" && format !== "json") {
|
|
22
|
-
|
|
23
|
-
process.stderr.write(`Total: ${meta.total}, showing: ${listLen}\n`);
|
|
110
|
+
process.stderr.write(`Total: ${meta.total}, showing: ${showing}\n`);
|
|
24
111
|
}
|
|
25
112
|
if (meta.partial === true) {
|
|
26
113
|
process.exitCode = 3;
|
|
27
114
|
}
|
|
28
115
|
}
|
|
116
|
+
const complete = process.exitCode !== 3;
|
|
29
117
|
if (output) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
118
|
+
const staged = format === "csv" || format === "jsonl"
|
|
119
|
+
? await stageExportMeta(output, format, streamed ? streamed.dataRows : countOutputRows(normalized, format), columns, normalized, complete)
|
|
120
|
+
: null;
|
|
121
|
+
try {
|
|
122
|
+
if (streamed) {
|
|
123
|
+
await streamed.finish();
|
|
124
|
+
}
|
|
125
|
+
else if (!(await streamOutputToFile(normalized, format, output))) {
|
|
126
|
+
warnIfLargeInMemory(items, format);
|
|
127
|
+
const content = renderOutput(normalized, format);
|
|
128
|
+
await saveOutputIfNeeded(format === "csv" ? `\ufeff${content}` : content, output);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await staged?.discard();
|
|
133
|
+
throw error;
|
|
33
134
|
}
|
|
34
|
-
|
|
35
|
-
const content = renderOutput(normalized, format);
|
|
36
|
-
await saveOutputIfNeeded(format === "csv" ? `\ufeff${content}` : content, output);
|
|
135
|
+
await staged?.commit();
|
|
37
136
|
process.stdout.write(`${output}\n`);
|
|
38
137
|
return;
|
|
39
138
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isStructuralError } from "./errors.js";
|
|
2
|
+
import { columnarSchemaValid } from "./normalize.js";
|
|
3
|
+
import { attachRowSink } from "./rowSink.js";
|
|
4
|
+
import { isVerbose, PAGE_CONCURRENCY, runInOrder } from "./transport.js";
|
|
2
5
|
const DAY_MS = 86_400_000;
|
|
3
6
|
const ALL_MARKET_LIMIT = 10_000;
|
|
4
7
|
function parseDate(value) {
|
|
@@ -12,6 +15,19 @@ function parseDate(value) {
|
|
|
12
15
|
function formatDate(d) {
|
|
13
16
|
return d.toISOString().slice(0, 10);
|
|
14
17
|
}
|
|
18
|
+
function columnRemap(header, shard) {
|
|
19
|
+
if (header.length === shard.length && header.every((field, i) => field === shard[i]))
|
|
20
|
+
return null;
|
|
21
|
+
const index = new Map(shard.map((field, i) => [String(field), i]));
|
|
22
|
+
const map = [];
|
|
23
|
+
for (const field of header) {
|
|
24
|
+
const i = index.get(String(field));
|
|
25
|
+
if (i === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
map.push(i);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
}
|
|
15
31
|
export function isFullMarket(body, fullMarketValue) {
|
|
16
32
|
const list = body.securityList;
|
|
17
33
|
if (!Array.isArray(list) || list.length !== 1)
|
|
@@ -79,7 +95,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
79
95
|
const failedShards = [];
|
|
80
96
|
let firstError = null;
|
|
81
97
|
let aborted = false;
|
|
82
|
-
const
|
|
98
|
+
const fetchShard = async (shard) => {
|
|
83
99
|
if (aborted) {
|
|
84
100
|
failedShards.push(shard);
|
|
85
101
|
return null;
|
|
@@ -95,43 +111,106 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
95
111
|
catch (error) {
|
|
96
112
|
if (!firstError)
|
|
97
113
|
firstError = error;
|
|
98
|
-
|
|
114
|
+
if (!isStructuralError(error))
|
|
115
|
+
aborted = true;
|
|
99
116
|
failedShards.push(shard);
|
|
100
117
|
return null;
|
|
101
118
|
}
|
|
102
|
-
}
|
|
119
|
+
};
|
|
120
|
+
const sink = client.claimRowSink?.();
|
|
103
121
|
let fieldList;
|
|
104
122
|
let header = null;
|
|
123
|
+
let fallback = null;
|
|
105
124
|
const merged = [];
|
|
125
|
+
let count = 0;
|
|
126
|
+
const objectKeys = new Set();
|
|
106
127
|
const truncatedShards = [];
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
const partialShards = [];
|
|
129
|
+
const keep = async (rows) => {
|
|
130
|
+
count += rows.length;
|
|
131
|
+
if (!fieldList)
|
|
132
|
+
for (const row of rows)
|
|
133
|
+
if (row && typeof row === "object" && !Array.isArray(row))
|
|
134
|
+
for (const key of Object.keys(row))
|
|
135
|
+
objectKeys.add(key);
|
|
136
|
+
if (sink)
|
|
137
|
+
await sink.push(rows);
|
|
138
|
+
else
|
|
139
|
+
for (const row of rows)
|
|
140
|
+
merged.push(row);
|
|
141
|
+
};
|
|
142
|
+
const mergeShard = async (r, i) => {
|
|
109
143
|
if (!(r && typeof r === "object"))
|
|
110
|
-
|
|
144
|
+
return;
|
|
111
145
|
const rec = r;
|
|
112
|
-
if (!header)
|
|
113
|
-
header = rec;
|
|
114
|
-
if (!fieldList && Array.isArray(rec.fieldList))
|
|
115
|
-
fieldList = rec.fieldList;
|
|
116
146
|
if (isTruncated(rec))
|
|
117
147
|
truncatedShards.push(shards[i]);
|
|
118
|
-
if (Array.isArray(rec.list))
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
148
|
+
if (!Array.isArray(rec.list))
|
|
149
|
+
return;
|
|
150
|
+
if (rec.list.length === 0) {
|
|
151
|
+
if ((typeof rec.total === "number" && rec.total > 0) || rec.partial === true) {
|
|
152
|
+
failedShards.push(shards[i]);
|
|
153
|
+
const claim = typeof rec.total === "number" && rec.total > 0 ? `reported total=${rec.total}` : "carried a partial marker";
|
|
154
|
+
process.stderr.write(`[gangtise] warning: shard ${shards[i].startDate}..${shards[i].endDate} ${claim} but delivered no rows; treated as failed (see failedShards)\n`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!fallback)
|
|
158
|
+
fallback = rec;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const shardFields = Array.isArray(rec.fieldList) && rec.fieldList.length > 0 ? rec.fieldList : undefined;
|
|
162
|
+
const columnar = rec.list.some(Array.isArray);
|
|
163
|
+
if (columnar && !columnarSchemaValid(shardFields, rec.list)) {
|
|
164
|
+
failedShards.push(shards[i]);
|
|
165
|
+
process.stderr.write(`[gangtise] warning: shard ${shards[i].startDate}..${shards[i].endDate} returned columnar rows that do not match its own fieldList (missing, duplicated or mis-sized); its rows were dropped (see failedShards)\n`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!fieldList && columnar && shardFields) {
|
|
169
|
+
fieldList = shardFields;
|
|
170
|
+
sink?.setFieldList(fieldList);
|
|
171
|
+
}
|
|
172
|
+
if (!header)
|
|
173
|
+
header = rec;
|
|
174
|
+
if (rec.partial === true)
|
|
175
|
+
partialShards.push(shards[i]);
|
|
176
|
+
const remap = columnar && fieldList && shardFields ? columnRemap(fieldList, shardFields) : null;
|
|
177
|
+
if (remap === undefined) {
|
|
178
|
+
failedShards.push(shards[i]);
|
|
179
|
+
process.stderr.write(`[gangtise] warning: shard ${shards[i].startDate}..${shards[i].endDate} returned columns that cannot be aligned with the first shard's fieldList; its rows were dropped (see failedShards)\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const rows = rec.list;
|
|
183
|
+
await keep(remap ? rows.map((item) => (Array.isArray(item) ? remap.map((k) => item[k]) : item)) : rows);
|
|
184
|
+
};
|
|
185
|
+
await runInOrder(shards, config.concurrency ?? PAGE_CONCURRENCY, fetchShard, mergeShard);
|
|
122
186
|
if (failedShards.length === shards.length) {
|
|
123
187
|
throw firstError ?? new Error(`All ${shards.length} kline shards failed`);
|
|
124
188
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
189
|
+
const base = header ?? fallback ?? {};
|
|
190
|
+
const out = { ...base, total: count, list: merged };
|
|
191
|
+
if (sink)
|
|
192
|
+
attachRowSink(out, sink);
|
|
193
|
+
if (fieldList) {
|
|
129
194
|
out.fieldList = fieldList;
|
|
195
|
+
}
|
|
196
|
+
else if (count > 0) {
|
|
197
|
+
out.fieldList = [...objectKeys];
|
|
198
|
+
}
|
|
199
|
+
else if (Array.isArray(base.fieldList)) {
|
|
200
|
+
out.fieldList = base.fieldList;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
delete out.fieldList;
|
|
204
|
+
}
|
|
130
205
|
if (failedShards.length > 0) {
|
|
131
206
|
out.partial = true;
|
|
132
207
|
out.failedShards = failedShards;
|
|
133
208
|
process.stderr.write(`[gangtise] warning: ${failedShards.length}/${shards.length} shards failed; results are partial (see failedShards)\n`);
|
|
134
209
|
}
|
|
210
|
+
if (partialShards.length > 0) {
|
|
211
|
+
out.partial = true;
|
|
212
|
+
process.stderr.write(`[gangtise] warning: ${partialShards.length}/${shards.length} shard(s) reported themselves partial; the merged result is marked partial\n`);
|
|
213
|
+
}
|
|
135
214
|
if (truncatedShards.length > 0) {
|
|
136
215
|
out.partial = true;
|
|
137
216
|
out.truncatedShards = truncatedShards;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertColumnarHeader, zipFieldRow } from "./normalize.js";
|
|
5
|
+
import { csvEscape, formatScalar, writeLine } from "./output.js";
|
|
6
|
+
import { extractTitles, MAX_TITLES_PER_ENDPOINT } from "./titleCache.js";
|
|
7
|
+
export const ROW_SINK = Symbol("gangtise.rowSink");
|
|
8
|
+
const STREAM_THRESHOLD = 1000;
|
|
9
|
+
export class ExportSink {
|
|
10
|
+
outputPath;
|
|
11
|
+
format;
|
|
12
|
+
cache;
|
|
13
|
+
rows = 0;
|
|
14
|
+
titles = {};
|
|
15
|
+
titleCount = 0;
|
|
16
|
+
buffer = [];
|
|
17
|
+
stream = null;
|
|
18
|
+
fields;
|
|
19
|
+
headerChecked = false;
|
|
20
|
+
finished = false;
|
|
21
|
+
columns = [];
|
|
22
|
+
columnSet = new Set();
|
|
23
|
+
objectRows = 0;
|
|
24
|
+
constructor(outputPath, format = "jsonl", cache) {
|
|
25
|
+
this.outputPath = outputPath;
|
|
26
|
+
this.format = format;
|
|
27
|
+
this.cache = cache;
|
|
28
|
+
}
|
|
29
|
+
get opened() {
|
|
30
|
+
return this.stream !== null;
|
|
31
|
+
}
|
|
32
|
+
get count() {
|
|
33
|
+
return this.rows + this.buffer.length;
|
|
34
|
+
}
|
|
35
|
+
get dataRows() {
|
|
36
|
+
if (this.format === "jsonl")
|
|
37
|
+
return this.rows;
|
|
38
|
+
return this.objectRows > 0 ? this.objectRows : this.rows;
|
|
39
|
+
}
|
|
40
|
+
get partPath() {
|
|
41
|
+
return `${this.outputPath}.part`;
|
|
42
|
+
}
|
|
43
|
+
get rowsPath() {
|
|
44
|
+
return `${this.outputPath}.rows.part`;
|
|
45
|
+
}
|
|
46
|
+
setFieldList(fields) {
|
|
47
|
+
this.fields = fields;
|
|
48
|
+
}
|
|
49
|
+
async push(rows) {
|
|
50
|
+
if (this.stream) {
|
|
51
|
+
await this.write(rows);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const row of rows)
|
|
55
|
+
this.buffer.push(row);
|
|
56
|
+
if (this.buffer.length < STREAM_THRESHOLD)
|
|
57
|
+
return;
|
|
58
|
+
await this.open();
|
|
59
|
+
const buffered = this.buffer;
|
|
60
|
+
this.buffer = [];
|
|
61
|
+
await this.write(buffered);
|
|
62
|
+
}
|
|
63
|
+
takeBuffer() {
|
|
64
|
+
const rows = this.buffer;
|
|
65
|
+
this.buffer = [];
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
async finish() {
|
|
69
|
+
const stream = this.stream;
|
|
70
|
+
if (!stream || this.finished)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
await endStream(stream);
|
|
74
|
+
if (this.format === "csv")
|
|
75
|
+
await this.assembleCsv();
|
|
76
|
+
await fs.rename(this.partPath, this.outputPath);
|
|
77
|
+
this.finished = true;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await this.abort();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async abort() {
|
|
85
|
+
const stream = this.stream;
|
|
86
|
+
if (!stream || this.finished)
|
|
87
|
+
return;
|
|
88
|
+
this.finished = true;
|
|
89
|
+
stream.destroy();
|
|
90
|
+
if (!stream.closed)
|
|
91
|
+
await new Promise((resolve) => stream.once("close", resolve));
|
|
92
|
+
await fs.unlink(this.partPath).catch(() => { });
|
|
93
|
+
await fs.unlink(this.rowsPath).catch(() => { });
|
|
94
|
+
}
|
|
95
|
+
async open() {
|
|
96
|
+
await fs.mkdir(path.dirname(this.outputPath), { recursive: true });
|
|
97
|
+
const stream = createWriteStream(this.format === "csv" ? this.rowsPath : this.partPath, { encoding: "utf8" });
|
|
98
|
+
stream.on("error", () => { });
|
|
99
|
+
this.stream = stream;
|
|
100
|
+
}
|
|
101
|
+
async write(rows) {
|
|
102
|
+
const stream = this.stream;
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
let item = row;
|
|
105
|
+
if (Array.isArray(row)) {
|
|
106
|
+
if (!this.headerChecked) {
|
|
107
|
+
assertColumnarHeader(this.fields);
|
|
108
|
+
this.headerChecked = true;
|
|
109
|
+
}
|
|
110
|
+
item = zipFieldRow(this.fields, row);
|
|
111
|
+
}
|
|
112
|
+
if (this.format === "csv" && isObjectRow(item)) {
|
|
113
|
+
this.objectRows++;
|
|
114
|
+
for (const key of Object.keys(item)) {
|
|
115
|
+
if (!this.columnSet.has(key)) {
|
|
116
|
+
this.columnSet.add(key);
|
|
117
|
+
this.columns.push(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await writeLine(stream, JSON.stringify(item));
|
|
122
|
+
this.rows++;
|
|
123
|
+
if (this.cache && this.titleCount < MAX_TITLES_PER_ENDPOINT) {
|
|
124
|
+
for (const [id, title] of Object.entries(extractTitles([item], this.cache))) {
|
|
125
|
+
if (!(id in this.titles))
|
|
126
|
+
this.titleCount++;
|
|
127
|
+
this.titles[id] = title;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async assembleCsv() {
|
|
133
|
+
const out = createWriteStream(this.partPath, { encoding: "utf8" });
|
|
134
|
+
out.on("error", () => { });
|
|
135
|
+
this.stream = out;
|
|
136
|
+
const objectMode = this.objectRows > 0;
|
|
137
|
+
const columns = objectMode ? this.columns : ["index", "value"];
|
|
138
|
+
await writeLine(out, "\ufeff" + columns.map(csvEscape).join(","));
|
|
139
|
+
const input = createReadStream(this.rowsPath, { encoding: "utf8" });
|
|
140
|
+
try {
|
|
141
|
+
let index = 0;
|
|
142
|
+
const emit = async (line) => {
|
|
143
|
+
if (!line)
|
|
144
|
+
return;
|
|
145
|
+
const item = JSON.parse(line);
|
|
146
|
+
if (objectMode) {
|
|
147
|
+
if (!isObjectRow(item))
|
|
148
|
+
return;
|
|
149
|
+
await writeLine(out, columns.map((column) => csvEscape(formatScalar(item[column]))).join(","));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
await writeLine(out, `${csvEscape(String(index++))},${csvEscape(formatScalar(item))}`);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
let rest = "";
|
|
156
|
+
for await (const chunk of input) {
|
|
157
|
+
const lines = (rest + String(chunk)).split("\n");
|
|
158
|
+
rest = lines.pop() ?? "";
|
|
159
|
+
for (const line of lines)
|
|
160
|
+
await emit(line);
|
|
161
|
+
}
|
|
162
|
+
await emit(rest);
|
|
163
|
+
await endStream(out);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
input.destroy();
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
await fs.unlink(this.rowsPath).catch(() => { });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function isObjectRow(value) {
|
|
173
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
174
|
+
}
|
|
175
|
+
function endStream(stream) {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
stream.end((err) => err ? reject(err) : resolve());
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
export function attachRowSink(result, sink) {
|
|
181
|
+
Object.defineProperty(result, ROW_SINK, { value: sink, enumerable: false, configurable: true });
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
export function getRowSink(value) {
|
|
185
|
+
if (!value || typeof value !== "object")
|
|
186
|
+
return undefined;
|
|
187
|
+
const sink = value[ROW_SINK];
|
|
188
|
+
return sink instanceof ExportSink ? sink : undefined;
|
|
189
|
+
}
|
|
190
|
+
export function rowCount(value) {
|
|
191
|
+
const sink = getRowSink(value);
|
|
192
|
+
if (sink)
|
|
193
|
+
return sink.count;
|
|
194
|
+
const list = value && typeof value === "object" ? value.list : undefined;
|
|
195
|
+
return Array.isArray(list) ? list.length : 0;
|
|
196
|
+
}
|
|
@@ -62,6 +62,57 @@ export async function runWithConcurrency(items, concurrency, fn) {
|
|
|
62
62
|
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
63
63
|
return results;
|
|
64
64
|
}
|
|
65
|
+
export async function runInOrder(items, concurrency, fn, consume) {
|
|
66
|
+
const width = Math.max(1, concurrency);
|
|
67
|
+
const pending = new Map();
|
|
68
|
+
let next = 0;
|
|
69
|
+
let failure = null;
|
|
70
|
+
let chain = Promise.resolve();
|
|
71
|
+
let waiters = [];
|
|
72
|
+
const wake = () => {
|
|
73
|
+
const ws = waiters;
|
|
74
|
+
waiters = [];
|
|
75
|
+
for (const w of ws)
|
|
76
|
+
w();
|
|
77
|
+
};
|
|
78
|
+
const drain = () => {
|
|
79
|
+
chain = chain.then(async () => {
|
|
80
|
+
while (!failure && pending.has(next)) {
|
|
81
|
+
const result = pending.get(next);
|
|
82
|
+
pending.delete(next);
|
|
83
|
+
await consume(result, next++);
|
|
84
|
+
wake();
|
|
85
|
+
}
|
|
86
|
+
}).catch((error) => {
|
|
87
|
+
failure ??= { error };
|
|
88
|
+
wake();
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
await runWithConcurrency(items, width, async (item, index) => {
|
|
92
|
+
if (failure)
|
|
93
|
+
return;
|
|
94
|
+
let result;
|
|
95
|
+
try {
|
|
96
|
+
result = await fn(item, index);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
failure ??= { error };
|
|
100
|
+
wake();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (failure)
|
|
104
|
+
return;
|
|
105
|
+
pending.set(index, result);
|
|
106
|
+
drain();
|
|
107
|
+
while (!failure && pending.size >= width) {
|
|
108
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
await chain;
|
|
112
|
+
const failed = failure;
|
|
113
|
+
if (failed)
|
|
114
|
+
throw failed.error;
|
|
115
|
+
}
|
|
65
116
|
export function resolvePageConcurrency(raw, fallback = 5, max = 32) {
|
|
66
117
|
const parsed = Math.floor(Number(raw));
|
|
67
118
|
if (!Number.isFinite(parsed) || parsed < 1)
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.38.0";
|