@cyanheads/eurostat-mcp-server 0.3.0 → 0.5.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.
Files changed (53) hide show
  1. package/CLAUDE.md +1 -1
  2. package/Dockerfile +16 -3
  3. package/README.md +50 -7
  4. package/dist/config/server-config.d.ts +2 -0
  5. package/dist/config/server-config.d.ts.map +1 -1
  6. package/dist/config/server-config.js +14 -0
  7. package/dist/config/server-config.js.map +1 -1
  8. package/dist/index.js +28 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts +40 -0
  11. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.d.ts.map +1 -0
  12. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js +113 -0
  13. package/dist/mcp-server/tools/definitions/eurostat-dataframe-describe.tool.js.map +1 -0
  14. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts +34 -0
  15. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.d.ts.map +1 -0
  16. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js +98 -0
  17. package/dist/mcp-server/tools/definitions/eurostat-dataframe-query.tool.js.map +1 -0
  18. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts +93 -0
  19. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.d.ts.map +1 -0
  20. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js +362 -0
  21. package/dist/mcp-server/tools/definitions/eurostat-download-dataset.tool.js.map +1 -0
  22. package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js +2 -2
  23. package/dist/mcp-server/tools/definitions/eurostat-get-dataset-info.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js +2 -2
  25. package/dist/mcp-server/tools/definitions/eurostat-get-dimension-values.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts +9 -0
  27. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js +68 -10
  29. package/dist/mcp-server/tools/definitions/eurostat-query-dataset.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js +3 -3
  31. package/dist/mcp-server/tools/definitions/eurostat-search-datasets.tool.js.map +1 -1
  32. package/dist/services/canvas-accessor.d.ts +31 -0
  33. package/dist/services/canvas-accessor.d.ts.map +1 -0
  34. package/dist/services/canvas-accessor.js +64 -0
  35. package/dist/services/canvas-accessor.js.map +1 -0
  36. package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts +89 -0
  37. package/dist/services/eurostat-bulk/eurostat-bulk-service.d.ts.map +1 -0
  38. package/dist/services/eurostat-bulk/eurostat-bulk-service.js +501 -0
  39. package/dist/services/eurostat-bulk/eurostat-bulk-service.js.map +1 -0
  40. package/dist/services/eurostat-bulk/types.d.ts +98 -0
  41. package/dist/services/eurostat-bulk/types.d.ts.map +1 -0
  42. package/dist/services/eurostat-bulk/types.js +82 -0
  43. package/dist/services/eurostat-bulk/types.js.map +1 -0
  44. package/dist/services/eurostat-data/eurostat-data-service.d.ts +63 -8
  45. package/dist/services/eurostat-data/eurostat-data-service.d.ts.map +1 -1
  46. package/dist/services/eurostat-data/eurostat-data-service.js +107 -13
  47. package/dist/services/eurostat-data/eurostat-data-service.js.map +1 -1
  48. package/dist/services/eurostat-data/types.d.ts +30 -0
  49. package/dist/services/eurostat-data/types.d.ts.map +1 -1
  50. package/dist/services/eurostat-data/types.js +4 -0
  51. package/dist/services/eurostat-data/types.js.map +1 -1
  52. package/package.json +2 -1
  53. package/server.json +85 -3
@@ -0,0 +1,501 @@
1
+ /**
2
+ * @fileoverview Eurostat Bulk Service — streaming client for the SDMX 2.1 TSV
3
+ * dissemination endpoint. Builds the positional dimension key, sniffs gzip off
4
+ * the stream, enforces a byte budget mid-transfer, classifies XML SOAP faults
5
+ * and the asynchronous queue envelope, and expands the wide TSV layout into
6
+ * one row per observation.
7
+ * @module services/eurostat-bulk/eurostat-bulk-service
8
+ */
9
+ import { notFound, serviceUnavailable, validationError, } from '@cyanheads/mcp-ts-core/errors';
10
+ import { fetchWithTimeout } from '@cyanheads/mcp-ts-core/utils';
11
+ import { getServerConfig } from '../../config/server-config.js';
12
+ import { CONF_STATUS_COLUMN, CONF_STATUS_LABEL_COLUMN, CONF_STATUS_LABELS, OBS_FLAG_COLUMN, OBS_FLAG_LABEL_COLUMN, OBS_FLAG_LABELS, OBS_VALUE_COLUMN, TIME_COLUMN, } from './types.js';
13
+ // Context satisfies the runtime contract of RequestContext but lacks the index signature
14
+ // required by fetchWithTimeout.
15
+ const asReqCtx = (ctx) => ctx;
16
+ /**
17
+ * Decoded characters buffered before an XML body is classified.
18
+ *
19
+ * Both non-data shapes are small — the largest SOAP fault observed is under 400
20
+ * bytes and the queue envelope under 350 — so this only has to hold one of them.
21
+ * It deliberately does **not** bound the search for the TSV header's line break:
22
+ * that line carries one field per period and runs to 166 KB on a daily series.
23
+ */
24
+ const XML_PEEK_CHARS = 8_192;
25
+ /** Separator between the observation flag and the confidentiality flag inside a TSV cell. */
26
+ const CONF_SEPARATOR = '@';
27
+ /** Eurostat's marker for an observation whose value is not available. */
28
+ const MISSING_VALUE = ':';
29
+ /** The header line's first field ends with this literal before the period columns begin. */
30
+ const TIME_HEADER_SUFFIX = '\\TIME_PERIOD';
31
+ /** Minimal XML entity decoding — the five predefined entities SOAP fault strings use. */
32
+ function decodeEntities(text) {
33
+ return text
34
+ .replaceAll('&lt;', '<')
35
+ .replaceAll('&gt;', '>')
36
+ .replaceAll('&quot;', '"')
37
+ .replaceAll('&apos;', "'")
38
+ .replaceAll('&amp;', '&');
39
+ }
40
+ /**
41
+ * Translate an XML body from the SDMX endpoint into the declared error contract.
42
+ *
43
+ * Two unrelated shapes arrive as XML and neither is data. A `S:Fault` envelope
44
+ * accompanies a 4xx and names the problem in a numeric `faultcode`. The
45
+ * `syncResponse` envelope arrives on **HTTP 200** carrying a queue ticket
46
+ * instead of the dataset — parsed as TSV it yields a header row of XML, so it is
47
+ * detected here rather than downstream.
48
+ *
49
+ * Always throws.
50
+ */
51
+ export function classifyXmlBody(xml, datasetCode) {
52
+ if (xml.includes('syncResponse') || xml.includes('<status>SUBMITTED</status>')) {
53
+ throw serviceUnavailable(`Eurostat queued this extraction instead of returning data: the SDMX endpoint answered with an asynchronous "SUBMITTED" ticket for "${datasetCode}". The queued result is not retrievable through this server. Narrow the request with dimension filters or a period range so Eurostat serves it synchronously.`, { reason: 'async_queued', datasetCode, retryable: false });
54
+ }
55
+ const code = /<faultcode>\s*(\d+)\s*<\/faultcode>/.exec(xml)?.[1];
56
+ const detail = decodeEntities(/<faultstring>([\s\S]*?)<\/faultstring>/.exec(xml)?.[1]?.trim() ?? xml.slice(0, 300));
57
+ if (code === '100') {
58
+ throw notFound(`Dataset "${datasetCode}" is not available for dissemination. Eurostat fault ${code}: ${detail}`, { reason: 'not_found', datasetCode, faultcode: code });
59
+ }
60
+ if (code === '140') {
61
+ throw validationError(`Eurostat rejected the dimension key: the number of filter positions did not match the dataset's dimensions. Eurostat fault ${code}: ${detail}`, { reason: 'filter_arity', datasetCode, faultcode: code });
62
+ }
63
+ if (code === '150') {
64
+ throw validationError(`Eurostat rejected a filter value. This fault also covers a period range outside the dataset's coverage. Eurostat fault ${code}: ${detail}`, { reason: 'invalid_dimension', datasetCode, faultcode: code });
65
+ }
66
+ throw serviceUnavailable(`Eurostat SDMX endpoint returned a fault for "${datasetCode}"${code ? ` (faultcode ${code})` : ''}: ${detail}`, { reason: 'upstream_fault', datasetCode, ...(code && { faultcode: code }) });
67
+ }
68
+ /**
69
+ * Split one wide-TSV cell into its value and flags.
70
+ *
71
+ * The grammar, read off live responses, is `<value>` `SPACE` `<flags>`, where
72
+ * flags is `<obs_flag>` optionally followed by `@<conf_status>` and either part
73
+ * may be empty: `429634.3 `, `3853937.0 p`, `: `, `: @C`. A cell that is empty
74
+ * on both sides of the split is not an observation at all and yields
75
+ * `undefined` — distinct from `: `, which is an observation Eurostat reports as
76
+ * unavailable.
77
+ */
78
+ export function parseCell(cell) {
79
+ const sep = cell.indexOf(' ');
80
+ const rawValue = (sep === -1 ? cell : cell.slice(0, sep)).trim();
81
+ const rawFlags = (sep === -1 ? '' : cell.slice(sep + 1)).trim();
82
+ if (rawValue === '' && rawFlags === '')
83
+ return;
84
+ const atIdx = rawFlags.indexOf(CONF_SEPARATOR);
85
+ const flag = (atIdx === -1 ? rawFlags : rawFlags.slice(0, atIdx)) || null;
86
+ const conf = (atIdx === -1 ? '' : rawFlags.slice(atIdx + 1)) || null;
87
+ if (rawValue === '' || rawValue === MISSING_VALUE)
88
+ return { conf, flag, value: null };
89
+ const num = Number(rawValue);
90
+ return { conf, flag, value: Number.isFinite(num) ? num : null };
91
+ }
92
+ /**
93
+ * Parse the header line of a wide TSV body.
94
+ *
95
+ * The first tab-separated field is the comma-joined dimension list with
96
+ * `\TIME_PERIOD` appended to the last name; the remaining fields are the period
97
+ * codes, each padded with a trailing space Eurostat does not strip.
98
+ */
99
+ export function parseHeader(line) {
100
+ const fields = line.split('\t');
101
+ const keyField = (fields[0] ?? '').trim();
102
+ const dimensionField = keyField.endsWith(TIME_HEADER_SUFFIX)
103
+ ? keyField.slice(0, -TIME_HEADER_SUFFIX.length)
104
+ : keyField;
105
+ const dimensions = dimensionField
106
+ .split(',')
107
+ .map((d) => d.trim())
108
+ .filter((d) => d !== '');
109
+ const periods = fields.slice(1).map((p) => p.trim());
110
+ if (dimensions.length === 0 || periods.length === 0) {
111
+ throw serviceUnavailable(`Eurostat returned a TSV body whose header names no ${dimensions.length === 0 ? 'dimensions' : 'periods'}. Header: ${line.slice(0, 200)}`, { reason: 'upstream_fault' });
112
+ }
113
+ return { dimensions, periods };
114
+ }
115
+ /**
116
+ * Build the positional dimension key the SDMX path takes.
117
+ *
118
+ * The key is a dot-separated segment carrying one position per dimension **in
119
+ * dataset order excluding time**; empty means wildcard and `+` means OR. The
120
+ * count must match exactly — a key with one position too few or too many is
121
+ * rejected upstream with faultcode 140 — so positions are emitted for every
122
+ * dimension in `dimensionOrder`, filtered or not, and never only for the
123
+ * dimensions the caller named.
124
+ *
125
+ * Returns `undefined` when no filter applies. An all-wildcard key is accepted
126
+ * by Eurostat, but omitting the segment asks the same question without staking
127
+ * the request on this server's copy of the dimension order being current.
128
+ */
129
+ export function buildKeyPath(dimensionOrder, filters) {
130
+ const applied = Object.entries(filters).filter(([, values]) => values.length > 0);
131
+ if (applied.length === 0)
132
+ return;
133
+ const unknown = applied.map(([dim]) => dim).filter((dim) => !dimensionOrder.includes(dim));
134
+ if (unknown.length > 0) {
135
+ throw validationError(`Filter names ${unknown.map((d) => `"${d}"`).join(', ')}, which ${unknown.length === 1 ? 'is not a dimension' : 'are not dimensions'} of this dataset. Filterable dimensions, in key order: ${dimensionOrder.join(', ')}. The time dimension is filtered with since_period/until_period instead.`, { reason: 'invalid_dimension', unknownDimensions: unknown, dimensionOrder });
136
+ }
137
+ return dimensionOrder.map((dim) => (filters[dim] ?? []).join('+')).join('.');
138
+ }
139
+ /**
140
+ * Canvas column schema for {@link BulkRow}, declared rather than sniffed.
141
+ *
142
+ * Inference reads only the leading rows, which types an all-integer measure
143
+ * prefix as `BIGINT` — serialized back out as a string — and types an
144
+ * all-missing prefix from nulls alone. Every column is nullable: a measure is
145
+ * routinely unavailable, and flags are the exception rather than the rule.
146
+ */
147
+ export function bulkRowSchema(dimensions) {
148
+ return [
149
+ ...dimensions.map((dim) => ({ name: dim, type: 'VARCHAR', nullable: true })),
150
+ { name: TIME_COLUMN, type: 'VARCHAR', nullable: true },
151
+ { name: OBS_VALUE_COLUMN, type: 'DOUBLE', nullable: true },
152
+ { name: OBS_FLAG_COLUMN, type: 'VARCHAR', nullable: true },
153
+ { name: OBS_FLAG_LABEL_COLUMN, type: 'VARCHAR', nullable: true },
154
+ { name: CONF_STATUS_COLUMN, type: 'VARCHAR', nullable: true },
155
+ { name: CONF_STATUS_LABEL_COLUMN, type: 'VARCHAR', nullable: true },
156
+ ];
157
+ }
158
+ export class EurostatBulkService {
159
+ // config and storage accepted to match the standard service init pattern;
160
+ // this service uses only the Eurostat public API and per-request config.
161
+ // biome-ignore lint/complexity/noUselessConstructor: standard init pattern
162
+ constructor(_config, _storage) { }
163
+ buildUrl(datasetCode, keyPath, sinceP, untilP) {
164
+ const { baseUrl } = getServerConfig();
165
+ const path = keyPath
166
+ ? `${encodeURIComponent(datasetCode)}/${encodeURIComponent(keyPath)}`
167
+ : encodeURIComponent(datasetCode);
168
+ const url = new URL(`${baseUrl}/sdmx/2.1/data/${path}`);
169
+ url.searchParams.set('format', 'TSV');
170
+ if (sinceP)
171
+ url.searchParams.set('startPeriod', sinceP);
172
+ if (untilP)
173
+ url.searchParams.set('endPeriod', untilP);
174
+ return url;
175
+ }
176
+ /**
177
+ * Start a bulk download: issue the request, classify the response, parse the
178
+ * header, and hand back a generator over the body.
179
+ *
180
+ * The request is made once with no retry. A bulk body streams for up to
181
+ * `EUROSTAT_BULK_TIMEOUT_MS`, so re-running it on a transient failure costs
182
+ * the caller another full transfer for a request that is already the most
183
+ * expensive one this server makes; the failure is surfaced instead.
184
+ */
185
+ async startDownload(datasetCode, dimensionOrder, filters, sinceP, untilP, ctx) {
186
+ const { bulkTimeoutMs, bulkMaxBytes } = getServerConfig();
187
+ const keyPath = buildKeyPath(dimensionOrder, filters);
188
+ const url = this.buildUrl(datasetCode, keyPath, sinceP, untilP);
189
+ ctx.log.info('Starting SDMX bulk download', {
190
+ datasetCode,
191
+ keyPath,
192
+ sinceP,
193
+ untilP,
194
+ maxBytes: bulkMaxBytes,
195
+ });
196
+ let response;
197
+ try {
198
+ response = await fetchWithTimeout(url.toString(), bulkTimeoutMs, asReqCtx(ctx), {
199
+ signal: ctx.signal,
200
+ // 400 (bad filter value or arity) and 404 (unknown dataset) are modeled outcomes
201
+ // reclassified below into the declared error contract, not service failures.
202
+ expectedStatuses: [400, 404],
203
+ });
204
+ }
205
+ catch (err) {
206
+ // fetchWithTimeout throws for any non-2xx before the caller sees the body, so the
207
+ // SOAP fault is only reachable through the captured excerpt on err.data. Faults run
208
+ // well under the framework's 500-byte cap, so the excerpt carries the whole envelope.
209
+ const body = err.data;
210
+ if (typeof body?.body === 'string' && body.body.includes('<')) {
211
+ classifyXmlBody(body.body, datasetCode);
212
+ }
213
+ throw err;
214
+ }
215
+ if (!response.body) {
216
+ throw serviceUnavailable(`Eurostat SDMX endpoint returned an empty body for "${datasetCode}".`, { reason: 'upstream_fault', datasetCode });
217
+ }
218
+ const stats = {
219
+ bytesRead: 0,
220
+ budgetExceeded: false,
221
+ compressed: false,
222
+ rowCount: 0,
223
+ missingCount: 0,
224
+ periodsSeen: [],
225
+ };
226
+ const decoded = await decodeBody(response.body, stats, bulkMaxBytes);
227
+ const { header, remainder, iterator } = await readHeader(decoded, datasetCode);
228
+ return {
229
+ header,
230
+ stats,
231
+ url: url.toString(),
232
+ rows: () => expandRows(iterator, remainder, header, stats),
233
+ };
234
+ }
235
+ }
236
+ /**
237
+ * Wrap the response body in a byte-budgeted, gzip-aware text stream.
238
+ *
239
+ * Eurostat compresses large bodies **without** a `Content-Encoding` header — the
240
+ * only header-level tell is a `.tsv.gz` filename on `Content-Disposition`, and
241
+ * that is not what is trusted here. The first two bytes are read off the stream
242
+ * and checked for the gzip magic number, so the decision is made on what
243
+ * actually arrived. The switch is not monotonic in dataset size, so it cannot
244
+ * be predicted from the request either.
245
+ *
246
+ * The budget counts bytes *after* decompression. That is the uniform measure
247
+ * across both encodings, and since a gzip body is never larger than what it
248
+ * expands to, bounding the decoded size bounds the transfer as well. Hitting it
249
+ * cancels the underlying reader rather than draining the rest — the body is
250
+ * chunked with no `Content-Length`, so a budget checked after the fact would
251
+ * have already paid for the whole transfer.
252
+ */
253
+ async function decodeBody(body, stats, maxBytes) {
254
+ const reader = body.getReader();
255
+ // Pull until at least the two magic bytes are in hand; a chunked body can deliver
256
+ // fewer on the first read, and an empty body delivers none.
257
+ const prefix = [];
258
+ let prefixBytes = 0;
259
+ while (prefixBytes < 2) {
260
+ const { done, value } = await reader.read();
261
+ if (done)
262
+ break;
263
+ if (value && value.length > 0) {
264
+ prefix.push(value);
265
+ prefixBytes += value.length;
266
+ }
267
+ }
268
+ const head = concatChunks(prefix, prefixBytes);
269
+ stats.compressed = head.length >= 2 && head[0] === 0x1f && head[1] === 0x8b;
270
+ const replayed = new ReadableStream({
271
+ start(controller) {
272
+ if (head.length > 0)
273
+ controller.enqueue(head);
274
+ },
275
+ async pull(controller) {
276
+ const { done, value } = await reader.read();
277
+ if (done)
278
+ controller.close();
279
+ else
280
+ controller.enqueue(value);
281
+ },
282
+ cancel(reason) {
283
+ return reader.cancel(reason);
284
+ },
285
+ });
286
+ /**
287
+ * `DecompressionStream`'s writable side is typed `BufferSource`, wider than the
288
+ * `Uint8Array` chunks this pipeline carries, so `pipeThrough` will not accept it
289
+ * directly. The cast narrows the transform to the stream types actually in play
290
+ * rather than widening the pipeline to `BufferSource`.
291
+ */
292
+ const byteStream = stats.compressed
293
+ ? replayed.pipeThrough(new DecompressionStream('gzip'))
294
+ : replayed;
295
+ return budgetedText(byteStream, stats, maxBytes);
296
+ }
297
+ function concatChunks(chunks, total) {
298
+ if (chunks.length === 1)
299
+ return chunks[0];
300
+ const out = new Uint8Array(total);
301
+ let offset = 0;
302
+ for (const chunk of chunks) {
303
+ out.set(chunk, offset);
304
+ offset += chunk.length;
305
+ }
306
+ return out;
307
+ }
308
+ /**
309
+ * Decode a byte stream to text, stopping the moment the byte budget is spent.
310
+ *
311
+ * The generator ends cleanly on overspend rather than throwing: the caller has
312
+ * already paid for everything downloaded so far, and discarding it would leave
313
+ * the agent with an error where it could have had a prefix of the dataset plus
314
+ * `budgetExceeded` telling it exactly what happened.
315
+ *
316
+ * Every exit cancels the reader, so nothing further is transferred: overspend,
317
+ * a normal end, and abandonment alike. Abandonment is the case that needs it —
318
+ * a staging failure or an abort part-way through the canvas append runs the
319
+ * `finally`, and releasing the lock alone would leave the response body
320
+ * undrained and its connection held. Cancelling a stream that already ended is
321
+ * a no-op.
322
+ */
323
+ async function* budgetedText(stream, stats, maxBytes) {
324
+ const reader = stream.getReader();
325
+ const decoder = new TextDecoder('utf-8');
326
+ try {
327
+ while (true) {
328
+ const { done, value } = await reader.read();
329
+ if (done)
330
+ break;
331
+ if (!value || value.length === 0)
332
+ continue;
333
+ stats.bytesRead += value.length;
334
+ if (stats.bytesRead > maxBytes) {
335
+ stats.budgetExceeded = true;
336
+ return;
337
+ }
338
+ const text = decoder.decode(value, { stream: true });
339
+ if (text)
340
+ yield text;
341
+ }
342
+ const tail = decoder.decode();
343
+ if (tail)
344
+ yield tail;
345
+ }
346
+ finally {
347
+ await reader.cancel().catch(() => undefined);
348
+ reader.releaseLock();
349
+ }
350
+ }
351
+ /**
352
+ * Consume enough of the text stream to tell data from XML, then take the header.
353
+ *
354
+ * Both non-data shapes announce themselves in the first character, so
355
+ * classification happens before a single row is parsed — a `syncResponse`
356
+ * envelope arrives on HTTP 200 and would otherwise be read as a TSV header of
357
+ * XML followed by no rows, which looks like a successful empty download.
358
+ *
359
+ * A body identified as XML is then buffered to the peek window before it is
360
+ * classified, rather than to the first line break. The XML declaration ends in a
361
+ * newline, so stopping there would hand the classifier the declaration alone and
362
+ * lose the `syncResponse` and `faultcode` markers that follow it.
363
+ *
364
+ * A TSV header, by contrast, is buffered to its line break with no length bound.
365
+ * Its length is proportional to the dataset's period count — 166 KB and 13,852
366
+ * fields on a daily series — so capping the search would parse a prefix of it as
367
+ * the whole header, silently drop every period column past the cap, and read the
368
+ * header's own tail as a data line. The byte budget is what bounds this loop.
369
+ */
370
+ async function readHeader(chunks, datasetCode) {
371
+ let buf = '';
372
+ let exhausted = false;
373
+ const pull = async () => {
374
+ const next = await chunks.next();
375
+ if (next.done)
376
+ exhausted = true;
377
+ else
378
+ buf += next.value;
379
+ };
380
+ while (buf.trim() === '' && !exhausted)
381
+ await pull();
382
+ if (buf.trimStart().startsWith('<')) {
383
+ while (buf.length < XML_PEEK_CHARS && !exhausted)
384
+ await pull();
385
+ classifyXmlBody(buf, datasetCode);
386
+ }
387
+ while (!buf.includes('\n') && !exhausted)
388
+ await pull();
389
+ const newline = buf.indexOf('\n');
390
+ if (newline === -1) {
391
+ if (buf.trim() === '') {
392
+ throw serviceUnavailable(`Eurostat SDMX endpoint returned an empty body for "${datasetCode}".`, { reason: 'upstream_fault', datasetCode });
393
+ }
394
+ // A body that ended without a line break: one header line and no data rows.
395
+ return { header: parseHeader(stripCr(buf)), iterator: chunks, remainder: '' };
396
+ }
397
+ return {
398
+ header: parseHeader(stripCr(buf.slice(0, newline))),
399
+ iterator: chunks,
400
+ remainder: buf.slice(newline + 1),
401
+ };
402
+ }
403
+ function stripCr(line) {
404
+ return line.endsWith('\r') ? line.slice(0, -1) : line;
405
+ }
406
+ /**
407
+ * Expand the wide TSV body into one row per populated cell.
408
+ *
409
+ * Each data line carries a comma-joined dimension key and one cell per period
410
+ * column, so a line becomes as many rows as it has populated cells. Cells that
411
+ * are blank on both sides of the value/flag split carry no observation and are
412
+ * skipped; `:` is kept, because Eurostat reporting a value as unavailable is
413
+ * itself data.
414
+ *
415
+ * Only complete lines are parsed. When the byte budget cancels the transfer
416
+ * mid-line the trailing fragment is dropped rather than parsed into a row with
417
+ * silently truncated fields.
418
+ */
419
+ async function* expandRows(chunks, remainder, header, stats) {
420
+ const seen = new Set();
421
+ let buf = remainder;
422
+ const emit = function* (line) {
423
+ const trimmed = stripCr(line);
424
+ if (trimmed === '')
425
+ return;
426
+ const fields = trimmed.split('\t');
427
+ const keyParts = (fields[0] ?? '').split(',');
428
+ const cellCount = Math.min(fields.length - 1, header.periods.length);
429
+ for (let i = 0; i < cellCount; i++) {
430
+ const parsed = parseCell(fields[i + 1] ?? '');
431
+ if (!parsed)
432
+ continue;
433
+ const period = header.periods[i] ?? '';
434
+ const row = {};
435
+ for (let d = 0; d < header.dimensions.length; d++) {
436
+ row[header.dimensions[d]] = keyParts[d]?.trim() ?? null;
437
+ }
438
+ row[TIME_COLUMN] = period;
439
+ row[OBS_VALUE_COLUMN] = parsed.value;
440
+ row[OBS_FLAG_COLUMN] = parsed.flag;
441
+ row[OBS_FLAG_LABEL_COLUMN] = parsed.flag ? (OBS_FLAG_LABELS[parsed.flag] ?? null) : null;
442
+ row[CONF_STATUS_COLUMN] = parsed.conf;
443
+ row[CONF_STATUS_LABEL_COLUMN] = parsed.conf
444
+ ? (CONF_STATUS_LABELS[parsed.conf] ?? null)
445
+ : null;
446
+ stats.rowCount++;
447
+ if (parsed.value === null)
448
+ stats.missingCount++;
449
+ seen.add(period);
450
+ yield row;
451
+ }
452
+ };
453
+ /**
454
+ * Drain every complete line sitting in the buffer. Called before the read loop
455
+ * as well as inside it: a small body arrives whole while the header is being
456
+ * read, leaving every data line already buffered and no further chunk to
457
+ * trigger a split.
458
+ */
459
+ const flush = function* () {
460
+ let newline = buf.indexOf('\n');
461
+ while (newline !== -1) {
462
+ yield* emit(buf.slice(0, newline));
463
+ buf = buf.slice(newline + 1);
464
+ newline = buf.indexOf('\n');
465
+ }
466
+ };
467
+ try {
468
+ yield* flush();
469
+ for await (const chunk of chunks) {
470
+ buf += chunk;
471
+ yield* flush();
472
+ }
473
+ // The final line of a complete body has no trailing newline. A budget-cancelled
474
+ // body's trailing fragment is not a final line — it is a line cut in half.
475
+ if (!stats.budgetExceeded)
476
+ yield* emit(buf);
477
+ stats.periodsSeen = [...seen].sort();
478
+ }
479
+ finally {
480
+ /**
481
+ * Close the text stream on every exit. A consumer that stops early — a canvas
482
+ * append that throws, an abort — abandons this generator, and a small body
483
+ * whose rows were all buffered while the header was read is abandoned before
484
+ * the `for await` above ever starts it. Neither path would otherwise reach the
485
+ * cancel that frees the response body.
486
+ */
487
+ await chunks.return(undefined);
488
+ }
489
+ }
490
+ // --- Init/accessor pattern ---
491
+ let _service;
492
+ export function initEurostatBulkService(config, storage) {
493
+ _service = new EurostatBulkService(config, storage);
494
+ }
495
+ export function getEurostatBulkService() {
496
+ if (!_service) {
497
+ throw new Error('EurostatBulkService not initialized — call initEurostatBulkService() in setup()');
498
+ }
499
+ return _service;
500
+ }
501
+ //# sourceMappingURL=eurostat-bulk-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eurostat-bulk-service.js","sourceRoot":"","sources":["../../../src/services/eurostat-bulk/eurostat-bulk-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @fileoverview Domain types and the static flag dictionaries for the Eurostat
3
+ * SDMX 2.1 bulk (TSV) service.
4
+ * @module services/eurostat-bulk/types
5
+ */
6
+ /**
7
+ * One observation flattened from the wide TSV layout into a dataframe row.
8
+ *
9
+ * The TSV endpoint carries codes only — no labels — so a row has one column per
10
+ * dimension holding the code, `time` for the period, then the measure columns.
11
+ * There is deliberately no `<dim>_label` companion: staging all-null label
12
+ * columns would claim a lookup the bulk endpoint never performed.
13
+ */
14
+ export type BulkRow = Record<string, string | number | null>;
15
+ /** Period column name. `time` matches the dimension name JSON-stat uses. */
16
+ export declare const TIME_COLUMN = "time";
17
+ /** Measure column names — the non-dimension columns of a {@link BulkRow}. */
18
+ export declare const OBS_VALUE_COLUMN = "obs_value";
19
+ export declare const OBS_FLAG_COLUMN = "obs_flag";
20
+ export declare const OBS_FLAG_LABEL_COLUMN = "obs_flag_label";
21
+ export declare const CONF_STATUS_COLUMN = "conf_status";
22
+ export declare const CONF_STATUS_LABEL_COLUMN = "conf_status_label";
23
+ /**
24
+ * Eurostat's `OBS_FLAG` codelist, verbatim from
25
+ * `sdmx/2.1/codelist/ESTAT/OBS_FLAG` — all 42 codes.
26
+ *
27
+ * Codes are composite: a cell flagged `bdep` is one code meaning "break in time
28
+ * series, definition differs, estimated, provisional", not four codes to be
29
+ * decomposed. Lookups that miss return `undefined` and the row's label column is
30
+ * `null` — Eurostat can ship a code in data that its published codelist omits,
31
+ * and an invented label is worse than an absent one.
32
+ */
33
+ export declare const OBS_FLAG_LABELS: Readonly<Record<string, string>>;
34
+ /**
35
+ * Eurostat's `CONF_STATUS` codelist, verbatim from
36
+ * `sdmx/2.1/codelist/ESTAT/CONF_STATUS` — all three codes.
37
+ *
38
+ * `C` is what appears in TSV data, behind the `@` of a cell like `: @C`. The
39
+ * same cells surface in JSON-stat folded into the observation status instead,
40
+ * as `|C` labelled `|confidential`, which is why a bulk-staged table and a
41
+ * query-staged one disagree about where a confidentiality marker lives.
42
+ */
43
+ export declare const CONF_STATUS_LABELS: Readonly<Record<string, string>>;
44
+ /**
45
+ * The header of a wide TSV body, parsed.
46
+ *
47
+ * Read from the response itself rather than from dataset metadata: the first
48
+ * field of the header line names the dimensions in the order the row keys use
49
+ * (`freq,unit,na_item,geo\TIME_PERIOD`), and the remaining tab-separated fields
50
+ * are the period columns. Both arrive with padding Eurostat does not strip.
51
+ */
52
+ export interface TsvHeader {
53
+ /** Dimension codes in row-key order, `time` excluded. */
54
+ dimensions: string[];
55
+ /** Period codes, in column order. */
56
+ periods: string[];
57
+ }
58
+ /**
59
+ * Counters filled in as {@link BulkDownload.rows} is drained.
60
+ *
61
+ * Nothing here is knowable before the stream is consumed — the body is chunked
62
+ * with no `Content-Length` — so a consumer reads these only after the generator
63
+ * has been exhausted (or has stopped on the byte budget).
64
+ */
65
+ export interface BulkStats {
66
+ /**
67
+ * True when the byte budget stopped the transfer before the body ended, so
68
+ * the rows are a prefix of the dataset rather than all of it.
69
+ */
70
+ budgetExceeded: boolean;
71
+ /** Decoded TSV bytes read. The unit the byte budget is enforced in. */
72
+ bytesRead: number;
73
+ /** Whether the response body arrived gzip-compressed. */
74
+ compressed: boolean;
75
+ /** Emitted observations carrying no numeric value. */
76
+ missingCount: number;
77
+ /** Distinct periods actually emitted, ascending. */
78
+ periodsSeen: string[];
79
+ /** Observations emitted — one per populated cell. */
80
+ rowCount: number;
81
+ }
82
+ /**
83
+ * A started bulk download: the header is parsed, the body is not yet drained.
84
+ *
85
+ * Returning before the body is consumed is what lets the caller declare a canvas
86
+ * column schema from {@link header} and then hand {@link rows} straight to the
87
+ * canvas appender, so a multi-million-row download never materializes as an
88
+ * array. {@link stats} is mutated as the generator runs and is only meaningful
89
+ * once it has finished.
90
+ */
91
+ export interface BulkDownload {
92
+ header: TsvHeader;
93
+ rows: () => AsyncGenerator<BulkRow>;
94
+ stats: BulkStats;
95
+ /** The request URL, minus nothing — no credentials are involved. */
96
+ url: string;
97
+ }
98
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/eurostat-bulk/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC;AAE7D,4EAA4E;AAC5E,eAAO,MAAM,WAAW,SAAS,CAAC;AAClC,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAC5C,eAAO,MAAM,eAAe,aAAa,CAAC;AAC1C,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AACtD,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAChD,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAE5D;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2C5D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI/D,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qCAAqC;IACrC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;IACpC,KAAK,EAAE,SAAS,CAAC;IACjB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;CACb"}