@lde/pipeline 0.31.4 → 0.32.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  A framework for transforming large RDF datasets, primarily using [SPARQL](https://www.w3.org/TR/sparql11-query/) queries with TypeScript for the parts that are hard to express in SPARQL alone.
4
4
 
5
5
  - **SPARQL-native.** Data transformations are plain SPARQL query files — portable, transparent, testable and version-controlled.
6
- - **Composable.** Executors are an interface: wrap a SPARQL executor with custom TypeScript to handle edge cases like date parsing or string normalisation (see [Executor](#executor)).
6
+ - **Composable.** Executors are an interface: wrap a SPARQL reader with custom TypeScript to handle edge cases like date parsing or string normalisation (see [Reader](#reader)).
7
7
  - **Extensible.** A plugin system lets packages like [@lde/pipeline-void](../pipeline-void) (or your own plugins) hook into the pipeline lifecycle.
8
8
 
9
9
  ## Components
@@ -50,7 +50,7 @@ const resolver = new ImportResolver(new SparqlDistributionResolver(), {
50
50
 
51
51
  ### Stage
52
52
 
53
- A stage groups an item selector, one or more executors, and configuration:
53
+ A stage groups an item selector, one or more readers, and configuration:
54
54
 
55
55
  ```typescript
56
56
  new Stage({
@@ -58,7 +58,7 @@ new Stage({
58
58
  itemSelector: new SparqlItemSelector({
59
59
  query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
60
60
  }),
61
- executors: executor,
61
+ readers: reader,
62
62
  batchSize: 100,
63
63
  maxConcurrency: 5,
64
64
  });
@@ -66,9 +66,9 @@ new Stage({
66
66
 
67
67
  #### Batch size
68
68
 
69
- `batchSize` (default: 10) controls how many variable bindings are passed to each executor call as a `VALUES` clause. It also sets the page size for the item selector's SPARQL requests, so that each paginated request fills exactly one executor batch.
69
+ `batchSize` (default: 10) controls how many variable bindings are passed to each reader call as a `VALUES` clause. It also sets the page size for the item selector's SPARQL requests, so that each paginated request fills exactly one reader batch.
70
70
 
71
- Some SPARQL endpoints enforce different result limits for SELECT and CONSTRUCT queries. Since the selector uses SELECT and the executor uses CONSTRUCT, a `LIMIT` clause in the selector query overrides `batchSize` as the page size. Use this when the endpoint caps SELECT results below your desired batch size:
71
+ Some SPARQL endpoints enforce different result limits for SELECT and CONSTRUCT queries. Since the selector uses SELECT and the reader uses CONSTRUCT, a `LIMIT` clause in the selector query overrides `batchSize` as the page size. Use this when the endpoint caps SELECT results below your desired batch size:
72
72
 
73
73
  ```typescript
74
74
  // Endpoint caps SELECT results at 500, but each CONSTRUCT can handle 1000 bindings.
@@ -77,14 +77,14 @@ new Stage({
77
77
  itemSelector: new SparqlItemSelector({
78
78
  query: 'SELECT DISTINCT ?class WHERE { ?s a ?class } LIMIT 500',
79
79
  }),
80
- executors: executor,
80
+ readers: reader,
81
81
  batchSize: 1000, // Two SELECT pages fill one CONSTRUCT batch.
82
82
  });
83
83
  ```
84
84
 
85
85
  #### Concurrency
86
86
 
87
- `maxConcurrency` (default: 10) limits the total number of concurrent SPARQL queries. Within each batch, all executors run in parallel; the number of concurrent batches is automatically reduced to `⌊maxConcurrency / executorCount⌋` so the total query pressure stays within the limit. For example, with `maxConcurrency: 10` and two executors per stage, up to 5 batches run concurrently (10 SPARQL queries total).
87
+ `maxConcurrency` (default: 10) limits the total number of concurrent SPARQL queries. Within each batch, all readers run in parallel; the number of concurrent batches is automatically reduced to `⌊maxConcurrency / executorCount⌋` so the total query pressure stays within the limit. For example, with `maxConcurrency: 10` and two readers per stage, up to 5 batches run concurrently (10 SPARQL queries total).
88
88
 
89
89
  #### Expecting output
90
90
 
@@ -94,7 +94,7 @@ Set it for scalar aggregates such as `SELECT (COUNT(*) AS ?n)`, which always ret
94
94
 
95
95
  ### Item Selector
96
96
 
97
- Selects resources from the distribution and fans out executor calls per batch of results. Implements the `ItemSelector` interface:
97
+ Selects resources from the distribution and fans out reader calls per batch of results. Implements the `ItemSelector` interface:
98
98
 
99
99
  ```typescript
100
100
  interface ItemSelector {
@@ -143,12 +143,12 @@ const itemSelector: ItemSelector = {
143
143
  };
144
144
  ```
145
145
 
146
- ### Executor
146
+ ### Reader
147
147
 
148
- Generates RDF triples. The built-in `SparqlConstructExecutor` runs a SPARQL CONSTRUCT query with template substitution and variable bindings:
148
+ Generates RDF triples. The built-in `SparqlConstructReader` runs a SPARQL CONSTRUCT query with template substitution and variable bindings:
149
149
 
150
150
  ```typescript
151
- const executor = new SparqlConstructExecutor({
151
+ const reader = new SparqlConstructReader({
152
152
  query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
153
153
  });
154
154
  ```
@@ -156,7 +156,7 @@ const executor = new SparqlConstructExecutor({
156
156
  When querying endpoints that return line-oriented formats like N-Triples (e.g. QLever), enable `lineBuffer` to work around an [N3.js chunk-splitting bug](https://github.com/rdfjs/N3.js/issues/578) that causes intermittent parse errors on large responses:
157
157
 
158
158
  ```typescript
159
- const executor = new SparqlConstructExecutor({
159
+ const reader = new SparqlConstructReader({
160
160
  query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
161
161
  lineBuffer: true,
162
162
  });
@@ -165,30 +165,30 @@ const executor = new SparqlConstructExecutor({
165
165
  SPARQL CONSTRUCT queries can produce duplicate triples — for example, constant triples (like `?dataset a edm:ProvidedCHO`) are emitted for every solution row. Enable `deduplicate` to remove duplicates inline on the stream using a string-based identity set (inspired by [Comunica's `distinctConstruct`](https://comunica.dev/docs/query/advanced/context/#14--distinct-construct)):
166
166
 
167
167
  ```typescript
168
- const executor = new SparqlConstructExecutor({
168
+ const reader = new SparqlConstructReader({
169
169
  query: 'CONSTRUCT { ?s a edm:ProvidedCHO . ?s ?p ?o } WHERE { ?s ?p ?o }',
170
170
  deduplicate: true,
171
171
  });
172
172
  ```
173
173
 
174
- The dedup set is scoped to each `execute()` call, so memory stays bounded to the number of unique quads per batch. A standalone `deduplicateQuads()` function is also exported for use outside the executor.
174
+ The dedup set is scoped to each `read()` call, so memory stays bounded to the number of unique quads per batch. A standalone `deduplicateQuads()` function is also exported for use outside the reader.
175
175
 
176
176
  ### Extending a stage with a quad transform
177
177
 
178
- Some logic is hard to express in pure SPARQL — cleaning up messy date notations, converting locale-specific dates to ISO 8601, or sampling an executor’s output and firing follow-up queries. Rather than subclass `Executor`, attach a `QuadTransform` to it as data: a plain function `(quads, context) => quads` that post-processes one executor’s output before the stage merges it with its siblings. This is extension point 1 of [ADR 2](../../docs/decisions/0002-unify-pipeline-extension-on-quad-transforms.md).
178
+ Some logic is hard to express in pure SPARQL — cleaning up messy date notations, converting locale-specific dates to ISO 8601, or sampling a reader’s output and firing follow-up queries. Rather than subclass `Reader`, attach a `QuadTransform` to it as data: a plain function `(quads, context) => quads` that post-processes one reader’s output before the stage merges it with its siblings. This is extension point 1 of [ADR 2](../../docs/decisions/0002-unify-pipeline-extension-on-quad-transforms.md).
179
179
 
180
- A transform receives an `ExecutorContext` — the `dataset`, the `distribution` (so it can fire its own SPARQL queries), and the `stage` name. It runs once per executor call, so **write it to accept being called more than once**: a global stage calls it once over the executor’s complete output, but a per-class stage with batching enabled calls it once per batch (one class at `batchSize: 1`). Accumulate within an invocation, not across invocations — or keep the transform per-quad, where the number of calls makes no difference.
180
+ A transform receives an `ReaderContext` — the `dataset`, the `distribution` (so it can fire its own SPARQL queries), and the `stage` name. It runs once per reader call, so **write it to accept being called more than once**: a global stage calls it once over the reader’s complete output, but a per-class stage with batching enabled calls it once per batch (one class at `batchSize: 1`). Accumulate within an invocation, not across invocations — or keep the transform per-quad, where the number of calls makes no difference.
181
181
 
182
182
  ```typescript
183
183
  import { DataFactory } from 'n3';
184
184
  import {
185
185
  Stage,
186
- SparqlConstructExecutor,
186
+ SparqlConstructReader,
187
187
  type QuadTransform,
188
- type ExecutorContext,
188
+ type ReaderContext,
189
189
  } from '@lde/pipeline';
190
190
 
191
- const cleanDates: QuadTransform<ExecutorContext> = async function* (quads) {
191
+ const cleanDates: QuadTransform<ReaderContext> = async function* (quads) {
192
192
  for await (const quad of quads) {
193
193
  if (quad.object.termType === 'Literal' && isMessyDate(quad.object)) {
194
194
  yield DataFactory.quad(
@@ -207,8 +207,8 @@ const cleanDates: QuadTransform<ExecutorContext> = async function* (quads) {
207
207
 
208
208
  new Stage({
209
209
  name: 'dates',
210
- executors: {
211
- executor: await SparqlConstructExecutor.fromFile('dates.rq'),
210
+ readers: {
211
+ reader: await SparqlConstructReader.fromFile('dates.rq'),
212
212
  transform: cleanDates,
213
213
  },
214
214
  });
@@ -252,11 +252,11 @@ Transitions are forwarded to the `ProgressReporter` via `timeoutTightened` / `ti
252
252
 
253
253
  Implement `TimeoutPolicy` directly for custom strategies (closing over shared state in the factory if you want it to span datasets).
254
254
 
255
- Timeouts live at the pipeline level — neither `SparqlConstructExecutor` nor `SparqlItemSelector` accept their own `timeout` option. Per-endpoint state belongs in the adaptive policy, and per-stage budgets aren’t supported. Reusable stage facades (`@lde/pipeline-void`, `@lde/pipeline-shacl-sampler`) follow the same convention.
255
+ Timeouts live at the pipeline level — neither `SparqlConstructReader` nor `SparqlItemSelector` accept their own `timeout` option. Per-endpoint state belongs in the adaptive policy, and per-stage budgets aren’t supported. Reusable stage facades (`@lde/pipeline-void`, `@lde/pipeline-shacl-sampler`) follow the same convention.
256
256
 
257
257
  ### Validation
258
258
 
259
- Stages can optionally validate their output quads against a `Validator`. Validation operates on the **combined output of all executors per batch**, not on individual quads or per-executor output. A batch produces a complete result set — a self-contained cluster of linked resources — that can be meaningfully matched against SHACL shapes. Even with a single executor, each batch is a complete unit; with multiple executors, shapes that reference triples from different executors are validated correctly.
259
+ Stages can optionally validate their output quads against a `Validator`. Validation operates on the **combined output of all readers per batch**, not on individual quads or per-reader output. A batch produces a complete result set — a self-contained cluster of linked resources — that can be meaningfully matched against SHACL shapes. Even with a single reader, each batch is a complete unit; with multiple readers, shapes that reference triples from different readers are validated correctly.
260
260
 
261
261
  Validating individual quads would be meaningless, since a single quad carries no structural context for shape matching. Validating the full pipeline output would also be problematic: because the pipeline streams results in batches, it doesn’t know where resource cluster boundaries fall. Batching the output could split a valid cluster across two batches, causing partial resources to fail validation even though the complete cluster is valid.
262
262
 
@@ -268,7 +268,7 @@ import { FileWriter } from '@lde/pipeline';
268
268
 
269
269
  new Stage({
270
270
  name: 'transform',
271
- executors: await SparqlConstructExecutor.fromFile('transform.rq'),
271
+ readers: await SparqlConstructReader.fromFile('transform.rq'),
272
272
  validation: {
273
273
  validator: new ShaclValidator({
274
274
  shapesFile: './shapes.ttl',
@@ -295,10 +295,26 @@ After all stages for a dataset have run, the pipeline calls `validator.report(da
295
295
 
296
296
  ### Writer
297
297
 
298
- Writes generated quads to a destination:
298
+ Writes generated quads to a destination. A `Writer` is transactional: each pipeline run opens one run on it (`openRun(context)`), writes every dataset through the resulting `RunWriter`, and ends with exactly one `commit()` (on success) or `abort(error)` (on failure). The run lifecycle is the home of destination-level concerns such as atomic swaps, deletion sweeps and cross-pod locks; the pipeline drives `openRun → write* → commit/abort` uniformly and never branches on the writer’s update mode.
299
299
 
300
- - `SparqlUpdateWriter` — writes to a SPARQL endpoint via UPDATE queries
301
- - `FileWriter` — writes to local files
300
+ - `SparqlUpdateWriter` — writes to a SPARQL endpoint via UPDATE queries; writes are visible as they land, so `commit`/`abort` are no-ops
301
+ - `FileWriter` — writes to local files; each file is streamed to a sibling temp file and atomically renamed on flush, `commit` finalizes any files still open, and `abort` discards temp output
302
+
303
+ The `RunContext` handed to `openRun` carries the run’s identity (`runId`, `startedAt`), the full selection (`selectedSources()`, complete by commit time – including datasets skipped as unchanged) and the pipeline’s provenance store, when configured.
304
+
305
+ A destination without run-level state implements the same contract with no-op `commit`/`abort` – `SparqlUpdateWriter` shows the pattern:
306
+
307
+ ```typescript
308
+ async openRun(): Promise<RunWriter> {
309
+ return {
310
+ write: async (dataset, quads) => {
311
+ // send quads somewhere
312
+ },
313
+ commit: () => Promise.resolve(),
314
+ abort: () => Promise.resolve(),
315
+ };
316
+ }
317
+ ```
302
318
 
303
319
  ### Reporter
304
320
 
@@ -425,7 +441,7 @@ new Pipeline({
425
441
  import {
426
442
  Pipeline,
427
443
  Stage,
428
- SparqlConstructExecutor,
444
+ SparqlConstructReader,
429
445
  SparqlItemSelector,
430
446
  SparqlUpdateWriter,
431
447
  ManualDatasetSelection,
@@ -439,7 +455,7 @@ const pipeline = new Pipeline({
439
455
  itemSelector: new SparqlItemSelector({
440
456
  query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
441
457
  }),
442
- executors: new SparqlConstructExecutor({
458
+ readers: new SparqlConstructReader({
443
459
  query:
444
460
  'CONSTRUCT { ?class a <http://example.org/Class> } WHERE { ?s a ?class }',
445
461
  }),
@@ -27,7 +27,7 @@ export interface PipelinePlugin {
27
27
  * Transform the merged, post-stage quad stream before writing (extension
28
28
  * point 2: pipeline-wide, post-merge). The home of cross-cutting concerns
29
29
  * – provenance, namespace normalisation – that apply regardless of which
30
- * executor produced a quad.
30
+ * reader produced a quad.
31
31
  */
32
32
  beforeStageWrite?: QuadTransform<BeforeStageWriteContext>;
33
33
  }
@@ -97,7 +97,7 @@ export declare class Pipeline {
97
97
  private reportValidators;
98
98
  private collectStages;
99
99
  /**
100
- * The writer a stage's merged output is written through: the user writer
100
+ * The writer a stage's merged output is written through: the open run
101
101
  * wrapped with the plugins' {@link PipelinePlugin.beforeStageWrite}
102
102
  * transforms, carrying this `stage`'s identity so a transform can mint stable
103
103
  * per-`(dataset, stage)` IRIs rather than blank nodes.
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAGrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EACL,KAAK,oBAAoB,EAI1B,MAAM,4BAA4B,CAAC;AAKpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAY7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,EAAE;QACT,mBAAmB,EAAE,mBAAmB,CAAC;QACzC,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,gBAAgB,GAAG,SAAS,gBAAgB,EAAE,CAAC;IAC1D;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,aAAa,CAAC;CAC/B;AAsFD,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAyC;IAC3E,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAuB;IAC5D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;gBAE9B,OAAO,EAAE,eAAe;IA+C9B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAoBZ,cAAc;IA6M5B,+EAA+E;YACjE,aAAa;YAmBb,gBAAgB;IAW9B,OAAO,CAAE,aAAa;IAOtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAMnB;;;;;;;;OAQG;IACH,OAAO,CAAC,0BAA0B;IA4BlC;;;;;OAKG;YACW,SAAS;IA8BvB;;;OAGG;YACW,QAAQ;IA0CtB,2EAA2E;YAC7D,eAAe;YAqBf,QAAQ;YA+DP,SAAS;CAczB"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAGrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAIV,MAAM,EACP,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,oBAAoB,EAI1B,MAAM,4BAA4B,CAAC;AAKpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAY7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,EAAE;QACT,mBAAmB,EAAE,mBAAmB,CAAC;QACzC,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,gBAAgB,GAAG,SAAS,gBAAgB,EAAE,CAAC;IAC1D;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,aAAa,CAAC;CAC/B;AAsHD,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAyC;IAC3E,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAuB;IAC5D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;gBAE9B,OAAO,EAAE,eAAe;IA+C9B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAyCZ,cAAc;IAqN5B,+EAA+E;YACjE,aAAa;YAmBb,gBAAgB;IAW9B,OAAO,CAAE,aAAa;IAOtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAMnB;;;;;;;;OAQG;IACH,OAAO,CAAC,0BAA0B;IA4BlC;;;;;OAKG;YACW,SAAS;IAuCvB;;;OAGG;YACW,QAAQ;IA0CtB,2EAA2E;YAC7D,eAAe;YAqBf,QAAQ;YAuEP,SAAS;CAczB"}
package/dist/pipeline.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createReadStream } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { StreamParser } from 'n3';
3
4
  import { FileWriter } from './writer/fileWriter.js';
4
5
  import { NoDistributionAvailable, ResolvedDistribution, } from './distribution/resolver.js';
@@ -8,7 +9,7 @@ import { shouldReprocess } from './provenance/reprocessDecision.js';
8
9
  import { NetworkError, SparqlProbeResult, } from '@lde/distribution-probe';
9
10
  import { ImportSuccessful } from '@lde/sparql-importer';
10
11
  import { importOutcomeToVerdict, probeResultToVerdict, } from '@lde/distribution-health';
11
- import { NotSupported } from './sparql/executor.js';
12
+ import { NotSupported } from './sparql/reader.js';
12
13
  import { combineReporters } from './combineReporters.js';
13
14
  import { ConstantTimeoutPolicy, } from './sparql/timeoutPolicy.js';
14
15
  /**
@@ -54,17 +55,46 @@ class FanOutWriter {
54
55
  constructor(writers) {
55
56
  this.writers = writers;
56
57
  }
58
+ async openRun(context) {
59
+ return new FanOutRunWriter(await Promise.all(this.writers.map((writer) => writer.openRun(context))));
60
+ }
61
+ }
62
+ /**
63
+ * Run independent branch operations concurrently, giving every branch its
64
+ * chance even when one fails; the first failure is rethrown afterwards.
65
+ */
66
+ async function settleBranches(tasks) {
67
+ const results = await Promise.allSettled(tasks);
68
+ const failed = results.find((result) => result.status === 'rejected');
69
+ if (failed)
70
+ throw failed.reason;
71
+ }
72
+ class FanOutRunWriter {
73
+ runs;
74
+ constructor(runs) {
75
+ this.runs = runs;
76
+ }
57
77
  async write(dataset, quads) {
58
- const branches = tee(quads, this.writers.length);
59
- await Promise.all(this.writers.map((writer, index) => writer.write(dataset, branches[index])));
78
+ const branches = tee(quads, this.runs.length);
79
+ await Promise.all(this.runs.map((run, index) => run.write(dataset, branches[index])));
60
80
  }
61
81
  async flush(dataset) {
62
- for (const w of this.writers)
63
- await w.flush?.(dataset);
82
+ for (const run of this.runs)
83
+ await run.flush?.(dataset);
64
84
  }
65
85
  async reset(dataset) {
66
- for (const w of this.writers)
67
- await w.reset?.(dataset);
86
+ for (const run of this.runs)
87
+ await run.reset?.(dataset);
88
+ }
89
+ async commit() {
90
+ // Destinations are independent, so their commits (alias swaps, sweeps –
91
+ // the slowest part of a run) need not queue behind each other.
92
+ await settleBranches(this.runs.map((run) => run.commit()));
93
+ }
94
+ async abort(error) {
95
+ // Abort every branch even when one abort fails: each destination must get
96
+ // its chance to clean up.
97
+ await settleBranches(this.runs.map((run) => run.abort(error)));
68
98
  }
69
99
  }
70
100
  class TransformWriter {
@@ -134,8 +164,28 @@ export class Pipeline {
134
164
  const selectStart = Date.now();
135
165
  const datasets = await this.datasetSelector.select();
136
166
  this.reporter?.datasetsSelected?.(datasets.total, Date.now() - selectStart);
137
- for await (const dataset of datasets) {
138
- await this.processDataset(dataset);
167
+ // The run transaction: one openRun → write* → commit/abort per pipeline
168
+ // run. `selectedSources` accumulates every selected dataset – including
169
+ // ones skipped as unchanged – so a writer's commit can sweep by registry
170
+ // membership.
171
+ const selectedSources = [];
172
+ const context = {
173
+ runId: randomUUID(),
174
+ startedAt: new Date().toISOString(),
175
+ selectedSources: () => selectedSources,
176
+ provenance: this.provenanceStore,
177
+ };
178
+ const runWriter = await this.writer.openRun(context);
179
+ try {
180
+ for await (const dataset of datasets) {
181
+ selectedSources.push(dataset.iri.toString());
182
+ await this.processDataset(dataset, runWriter, context);
183
+ }
184
+ await runWriter.commit();
185
+ }
186
+ catch (error) {
187
+ await runWriter.abort(error);
188
+ throw error;
139
189
  }
140
190
  const finalMemory = process.memoryUsage();
141
191
  this.reporter?.pipelineComplete?.({
@@ -144,7 +194,7 @@ export class Pipeline {
144
194
  heapUsedBytes: finalMemory.heapUsed,
145
195
  });
146
196
  }
147
- async processDataset(dataset) {
197
+ async processDataset(dataset, runWriter, context) {
148
198
  this.reporter?.datasetStart?.(dataset);
149
199
  // Probe phase: gather probe results and the source-to-be, without importing.
150
200
  let probed;
@@ -232,7 +282,7 @@ export class Pipeline {
232
282
  });
233
283
  let stageFailed = false;
234
284
  try {
235
- stageFailed = await this.runStages(dataset, resolved.distribution, timeout);
285
+ stageFailed = await this.runStages(dataset, resolved.distribution, timeout, runWriter, context);
236
286
  // Reactive fallback: an endpoint that passed probing but could not serve
237
287
  // the analysis stages is empirically incapable. Switch to the dataset’s
238
288
  // data dump and re-run all stages locally, discarding the
@@ -265,8 +315,8 @@ export class Pipeline {
265
315
  this.reportSelectedDistribution(dataset, fallback, fingerprint);
266
316
  // Discard the endpoint-sourced partial output before the re-run so
267
317
  // the dump-sourced stats replace it rather than appending to it.
268
- await this.writer.reset?.(dataset);
269
- stageFailed = await this.runStages(dataset, fallback.distribution, timeout);
318
+ await runWriter.reset?.(dataset);
319
+ stageFailed = await this.runStages(dataset, fallback.distribution, timeout, runWriter, context);
270
320
  }
271
321
  else if (fallback.importFailed) {
272
322
  // A failed dump import is a deep validity verdict on that dump –
@@ -284,7 +334,7 @@ export class Pipeline {
284
334
  await this.distributionResolver.cleanup?.();
285
335
  unsubscribe?.();
286
336
  }
287
- await this.writer.flush?.(dataset);
337
+ await runWriter.flush?.(dataset);
288
338
  await this.reportValidators(dataset);
289
339
  // A dataset whose stages threw produced incomplete output; record it as
290
340
  // ‘failed’ rather than freezing a broken result under a ‘success’ record.
@@ -331,15 +381,15 @@ export class Pipeline {
331
381
  }
332
382
  }
333
383
  /**
334
- * The writer a stage's merged output is written through: the user writer
384
+ * The writer a stage's merged output is written through: the open run
335
385
  * wrapped with the plugins' {@link PipelinePlugin.beforeStageWrite}
336
386
  * transforms, carrying this `stage`'s identity so a transform can mint stable
337
387
  * per-`(dataset, stage)` IRIs rather than blank nodes.
338
388
  */
339
- stageWriter(stage) {
389
+ stageWriter(runWriter, stage) {
340
390
  return this.beforeStageWrite
341
- ? new TransformWriter(this.writer, this.beforeStageWrite, stage)
342
- : this.writer;
391
+ ? new TransformWriter(runWriter, this.beforeStageWrite, stage)
392
+ : runWriter;
343
393
  }
344
394
  /**
345
395
  * Report a resolved distribution as the dataset's selected source, plus its
@@ -362,15 +412,15 @@ export class Pipeline {
362
412
  * whether any stage hard-failed – the signal the reactive dump fallback
363
413
  * reacts to.
364
414
  */
365
- async runStages(dataset, distribution, timeout) {
415
+ async runStages(dataset, distribution, timeout, runWriter, context) {
366
416
  let stageFailed = false;
367
417
  for (const stage of this.stages) {
368
418
  try {
369
419
  if (stage.stages.length > 0) {
370
- await this.runChain(dataset, distribution, stage, timeout);
420
+ await this.runChain(dataset, distribution, stage, runWriter, context, timeout);
371
421
  }
372
422
  else {
373
- await this.runStage(dataset, distribution, stage, this.stageWriter(stage.name), timeout);
423
+ await this.runStage(dataset, distribution, stage, this.stageWriter(runWriter, stage.name), timeout);
374
424
  }
375
425
  }
376
426
  catch (error) {
@@ -384,7 +434,7 @@ export class Pipeline {
384
434
  * Run a stage with reporting and return whether it was supported.
385
435
  * Returns `true` if the stage produced results, `false` if NotSupported.
386
436
  */
387
- async runStage(dataset, distribution, stage, writer = this.writer, timeout) {
437
+ async runStage(dataset, distribution, stage, writer, timeout) {
388
438
  this.reporter?.stageStart?.(stage.name);
389
439
  const stageStart = Date.now();
390
440
  let itemsProcessed = 0;
@@ -421,34 +471,46 @@ export class Pipeline {
421
471
  throw new Error(`Stage '${stage.name}' returned NotSupported in chained mode`);
422
472
  }
423
473
  }
424
- async runChain(dataset, distribution, stage, timeout) {
474
+ async runChain(dataset, distribution, stage, runWriter, context, timeout) {
425
475
  const { stageOutputResolver, outputDir } = this.chaining;
426
476
  const outputFiles = [];
427
- try {
428
- // 1. Run parent stage → FileWriter.
429
- const parentWriter = new FileWriter({
430
- outputDir: `${outputDir}/${stage.name}`,
477
+ // Run one chained stage into its scratch FileWriter and flush it, so the
478
+ // output file exists on its final path before the resolver reads it. The
479
+ // scratch run is bracketed like any other: committed on success, aborted
480
+ // on failure so no half-written temp file is left behind.
481
+ const runScratchStage = async (chainedStage, stageDistribution) => {
482
+ const scratchWriter = new FileWriter({
483
+ outputDir: `${outputDir}/${chainedStage.name}`,
431
484
  format: 'n-triples',
432
485
  });
433
- await this.runChainedStage(dataset, distribution, stage, parentWriter, timeout);
434
- outputFiles.push(parentWriter.getOutputPath(dataset));
486
+ const scratchRun = await scratchWriter.openRun(context);
487
+ try {
488
+ await this.runChainedStage(dataset, stageDistribution, chainedStage, scratchRun, timeout);
489
+ await scratchRun.flush(dataset);
490
+ await scratchRun.commit();
491
+ }
492
+ catch (error) {
493
+ await scratchRun.abort(error);
494
+ throw error;
495
+ }
496
+ return scratchWriter.getOutputPath(dataset);
497
+ };
498
+ try {
499
+ // 1. Run parent stage → scratch FileWriter.
500
+ const parentOutput = await runScratchStage(stage, distribution);
501
+ outputFiles.push(parentOutput);
435
502
  // 2. Chain through children.
436
- let currentDistribution = await stageOutputResolver.resolve(parentWriter.getOutputPath(dataset));
503
+ let currentDistribution = await stageOutputResolver.resolve(parentOutput);
437
504
  for (let i = 0; i < stage.stages.length; i++) {
438
- const child = stage.stages[i];
439
- const childWriter = new FileWriter({
440
- outputDir: `${outputDir}/${child.name}`,
441
- format: 'n-triples',
442
- });
443
- await this.runChainedStage(dataset, currentDistribution, child, childWriter, timeout);
444
- outputFiles.push(childWriter.getOutputPath(dataset));
505
+ const childOutput = await runScratchStage(stage.stages[i], currentDistribution);
506
+ outputFiles.push(childOutput);
445
507
  if (i < stage.stages.length - 1) {
446
- currentDistribution = await stageOutputResolver.resolve(childWriter.getOutputPath(dataset));
508
+ currentDistribution = await stageOutputResolver.resolve(childOutput);
447
509
  }
448
510
  }
449
- // 3. Concatenate all output files → user writer, applying the plugins'
511
+ // 3. Concatenate all output files → the open run, applying the plugins'
450
512
  // beforeStageWrite transforms once for the chain under the parent stage.
451
- await this.stageWriter(stage.name).write(dataset, this.readFiles(outputFiles));
513
+ await this.stageWriter(runWriter, stage.name).write(dataset, this.readFiles(outputFiles));
452
514
  }
453
515
  finally {
454
516
  await stageOutputResolver.cleanup();
@@ -1 +1 @@
1
- {"version":3,"file":"fileLoadedSparqlProvenanceStore.d.ts","sourceRoot":"","sources":["../../src/provenance/fileLoadedSparqlProvenanceStore.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAI9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAgBlD,MAAM,WAAW,sCAAsC;IACrD,wEAAwE;IACxE,aAAa,EAAE,GAAG,CAAC;IACnB;;;OAGG;IACH,WAAW,EAAE,GAAG,CAAC;IACjB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,qBAAa,+BAAgC,YAAW,eAAe;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEpC,OAAO,EAAE,sCAAsC;IAWrD,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAoB5D,OAAO,CAAC,WAAW;IAmBb,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;YAMpD,OAAO;CAsBvB"}
1
+ {"version":3,"file":"fileLoadedSparqlProvenanceStore.d.ts","sourceRoot":"","sources":["../../src/provenance/fileLoadedSparqlProvenanceStore.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAI9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAgBlD,MAAM,WAAW,sCAAsC;IACrD,wEAAwE;IACxE,aAAa,EAAE,GAAG,CAAC;IACnB;;;OAGG;IACH,WAAW,EAAE,GAAG,CAAC;IACjB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,qBAAa,+BAAgC,YAAW,eAAe;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEpC,OAAO,EAAE,sCAAsC;IAWrD,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAoB5D,OAAO,CAAC,WAAW;IAmBb,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;YASpD,OAAO;CAsBvB"}
@@ -68,8 +68,11 @@ export class FileLoadedSparqlProvenanceStore {
68
68
  }
69
69
  async set(datasetUri, record) {
70
70
  const dataset = new Dataset({ iri: datasetUri, distributions: [] });
71
- await this.writer.write(dataset, this.toQuads(datasetUri, record));
72
- await this.writer.flush(dataset);
71
+ // Each record is complete in itself, so it gets its own mini run:
72
+ // write + flush materializes the record file atomically.
73
+ const run = await this.writer.openRun();
74
+ await run.write(dataset, this.toQuads(datasetUri, record));
75
+ await run.flush(dataset);
73
76
  }
74
77
  async *toQuads(datasetUri, record) {
75
78
  const subject = namedNode(datasetUri.toString());
@@ -1,4 +1,4 @@
1
- export { deduplicateQuads, SparqlConstructExecutor, LineBufferTransform, NotSupported, readQueryFile, type ExecuteOptions, type Executor, type SparqlConstructExecutorOptions, type VariableBindings, } from './executor.js';
1
+ export { deduplicateQuads, SparqlConstructReader, LineBufferTransform, NotSupported, readQueryFile, type ReadOptions, type Reader, type SparqlConstructReaderOptions, type VariableBindings, } from './reader.js';
2
2
  export { SparqlItemSelector, type SparqlItemSelectorOptions, } from './selector.js';
3
3
  export { injectValues } from './values.js';
4
4
  export { withDefaultGraph } from './graph.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sparql/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,8BAA8B,EACnC,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sparql/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,KAAK,4BAA4B,EACjC,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,oBAAoB,CAAC"}
@@ -1,4 +1,4 @@
1
- export { deduplicateQuads, SparqlConstructExecutor, LineBufferTransform, NotSupported, readQueryFile, } from './executor.js';
1
+ export { deduplicateQuads, SparqlConstructReader, LineBufferTransform, NotSupported, readQueryFile, } from './reader.js';
2
2
  export { SparqlItemSelector, } from './selector.js';
3
3
  export { injectValues } from './values.js';
4
4
  export { withDefaultGraph } from './graph.js';