@lde/pipeline 0.34.1 → 0.34.3

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.
@@ -1,4 +1,5 @@
1
1
  import { Dataset } from '@lde/dataset';
2
+ import type { Quad } from '@rdfjs/types';
2
3
  import type { DatasetSelector } from './selector.js';
3
4
  import { Stage } from './stage.js';
4
5
  import type { QuadTransform } from './stage.js';
@@ -23,7 +24,7 @@ export interface BeforeStageWriteContext {
23
24
  /**
24
25
  * Context handed to a {@link PipelinePlugin.beforeDatasetWrite} transform: the
25
26
  * `dataset` whose complete, cross-stage output is being written. Unlike
26
- * {@link BeforeStageWriteContext} there is no `stage` — the transform sees one
27
+ * {@link BeforeStageWriteContext} there is no `stage` – the transform sees one
27
28
  * dataset's quads from every stage as a single stream.
28
29
  */
29
30
  export interface BeforeDatasetWriteContext {
@@ -36,7 +37,7 @@ export interface PipelinePlugin {
36
37
  * Transform a single stage's merged output before writing (extension point
37
38
  * 2: per stage, post-merge). Runs once per stage per dataset, carrying the
38
39
  * stage identity so it can mint stable per-`(dataset, stage)` IRIs. The home
39
- * of stage-scoped concerns — provenance stamping, namespace rewriting of a
40
+ * of stage-scoped concerns – provenance stamping, namespace rewriting of a
40
41
  * stage's own quads.
41
42
  */
42
43
  beforeStageWrite?: QuadTransform<BeforeStageWriteContext>;
@@ -45,7 +46,7 @@ export interface PipelinePlugin {
45
46
  * dataset, cross-stage). Runs once over the concatenation of every stage's
46
47
  * output for one dataset, after all stages complete and before the write is
47
48
  * flushed. The home of concerns that need to reconcile quads *across* stages
48
- * — deduplication, cross-stage roll-ups, or merging partition nodes that
49
+ * – deduplication, cross-stage roll-ups, or merging partition nodes that
49
50
  * different stages contribute to.
50
51
  *
51
52
  * The transform is a streaming {@link QuadTransform}: the pipeline pipes each
@@ -55,11 +56,19 @@ export interface PipelinePlugin {
55
56
  */
56
57
  beforeDatasetWrite?: QuadTransform<BeforeDatasetWriteContext>;
57
58
  }
58
- export interface PipelineOptions {
59
+ export interface PipelineOptions<Out = Quad> {
59
60
  datasetSelector: DatasetSelector;
60
- stages: Stage[];
61
- writers: Writer | Writer[];
62
- plugins?: PipelinePlugin[];
61
+ stages: Stage<Out>[];
62
+ writers: Writer<Out> | Writer<Out>[];
63
+ /**
64
+ * Quad-level plugins ({@link PipelinePlugin}). Available only to a plain quad
65
+ * pipeline: a projecting pipeline's write-side data is already the projected
66
+ * item, and both plugin points are `QuadTransform`s with no quads to act on,
67
+ * so the type forbids them (`never`). Reader-attached quad transforms are
68
+ * unaffected – they run before the projection seam. See
69
+ * {@link https://github.com/ldelements/lde/blob/main/docs/decisions/0013-project-inside-the-batch-per-root-type.md | ADR 13}.
70
+ */
71
+ plugins?: [Out] extends [Quad] ? PipelinePlugin[] : never;
63
72
  name?: string;
64
73
  distributionResolver?: DistributionResolver;
65
74
  chaining?: {
@@ -101,7 +110,7 @@ export interface PipelineOptions {
101
110
  */
102
111
  timeout?: () => TimeoutPolicy;
103
112
  }
104
- export declare class Pipeline {
113
+ export declare class Pipeline<Out = Quad> {
105
114
  private readonly name;
106
115
  private readonly datasetSelector;
107
116
  private readonly stages;
@@ -114,7 +123,7 @@ export declare class Pipeline {
114
123
  private readonly timeoutFactory;
115
124
  private readonly provenanceStore?;
116
125
  private readonly pipelineVersion?;
117
- constructor(options: PipelineOptions);
126
+ constructor(options: PipelineOptions<Out>);
118
127
  run(): Promise<void>;
119
128
  private processDataset;
120
129
  /** Persist the processing record for a dataset, when a store is configured. */
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
package/dist/pipeline.js CHANGED
@@ -60,7 +60,7 @@ class FanOutWriter {
60
60
  // Opening a writer's run can have side effects (a cross-pod lock, a fresh
61
61
  // collection). If one writer opens but a sibling then fails, the pipeline
62
62
  // never gets a RunWriter to abort, so roll the opened ones back here
63
- // before rethrowing — otherwise their locks and collections leak.
63
+ // before rethrowing – otherwise their locks and collections leak.
64
64
  const results = await Promise.allSettled(this.writers.map((writer) => writer.openRun(context)));
65
65
  const opened = results.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
66
66
  const failure = results.find((result) => result.status === 'rejected');
@@ -86,8 +86,8 @@ class FanOutRunWriter {
86
86
  constructor(runs) {
87
87
  this.runs = runs;
88
88
  }
89
- async write(dataset, quads) {
90
- const branches = tee(quads, this.runs.length);
89
+ async write(dataset, items) {
90
+ const branches = tee(items, this.runs.length);
91
91
  await Promise.all(this.runs.map((run, index) => run.write(dataset, branches[index])));
92
92
  }
93
93
  async flush(dataset, outcome) {
@@ -241,13 +241,17 @@ export class Pipeline {
241
241
  this.writer = Array.isArray(options.writers)
242
242
  ? new FanOutWriter(options.writers)
243
243
  : options.writers;
244
- const transforms = options.plugins
244
+ // Plugins are quad-typed and only reachable when `Out` is `Quad` (the
245
+ // `plugins` field is `never` otherwise), but inside the class body `Out` is
246
+ // opaque, so the conditional-typed field is read as a plain plugin array.
247
+ const plugins = options.plugins;
248
+ const transforms = plugins
245
249
  ?.map((p) => p.beforeStageWrite)
246
250
  .filter((t) => t !== undefined);
247
251
  this.beforeStageWrite = transforms?.length
248
252
  ? (quads, context) => transforms.reduce((q, fn) => fn(q, context), quads)
249
253
  : undefined;
250
- const datasetTransforms = options.plugins
254
+ const datasetTransforms = plugins
251
255
  ?.map((p) => p.beforeDatasetWrite)
252
256
  .filter((t) => t !== undefined);
253
257
  this.beforeDatasetWrite = datasetTransforms?.length
@@ -285,7 +289,10 @@ export class Pipeline {
285
289
  };
286
290
  const openedWriter = await this.writer.openRun(context);
287
291
  // Wrap the run writer so a whole-dataset transform sees every stage's
288
- // output for a dataset as one stream before it is written.
292
+ // output for a dataset as one stream before it is written. The transform is
293
+ // quad-typed and only present when `Out` is `Quad` (a projecting pipeline
294
+ // has no plugins), so narrowing the run writer to `RunWriter<Quad>` here is
295
+ // sound – tsc cannot see that invariant.
289
296
  const runWriter = this.beforeDatasetWrite
290
297
  ? new DatasetTransformWriter(openedWriter, this.beforeDatasetWrite)
291
298
  : openedWriter;
@@ -495,6 +502,8 @@ export class Pipeline {
495
502
  this.reporter?.datasetValidated?.(dataset, report);
496
503
  }
497
504
  }
505
+ // Walks every stage and sub-stage to collect validators; it reads only
506
+ // `validator`/`stages`, never output, so it is agnostic to the item type.
498
507
  *collectStages(stages) {
499
508
  for (const stage of stages) {
500
509
  yield stage;
@@ -509,6 +518,9 @@ export class Pipeline {
509
518
  * per-`(dataset, stage)` IRIs rather than blank nodes.
510
519
  */
511
520
  stageWriter(runWriter, stage) {
521
+ // `beforeStageWrite` is quad-typed and only present when `Out` is `Quad` (a
522
+ // projecting pipeline has no plugins), so the `TransformWriter` wrap is
523
+ // reached only in that case; tsc cannot see the invariant, hence the casts.
512
524
  return this.beforeStageWrite
513
525
  ? new TransformWriter(runWriter, this.beforeStageWrite, stage)
514
526
  : runWriter;
@@ -596,10 +608,12 @@ export class Pipeline {
596
608
  async runChain(dataset, distribution, stage, runWriter, context, timeout) {
597
609
  const { stageOutputResolver, outputDir } = this.chaining;
598
610
  const outputFiles = [];
599
- // Run one chained stage into its scratch FileWriter and flush it, so the
600
- // output file exists on its final path before the resolver reads it. The
601
- // scratch run is bracketed like any other: committed on success, aborted
602
- // on failure so no half-written temp file is left behind.
611
+ // Chaining is a quad-only concern: every stage in a chain writes N-Triples
612
+ // to a scratch FileWriter (a `Writer<Quad>`), and a chained parent cannot
613
+ // project (a stage with `stages` forbids `project`), so it emits quads. The
614
+ // requirement "sub-stages imply `Out` is `Quad`" is not expressible – `stages`
615
+ // and `chaining` are independent options – so it is a runtime contract, and
616
+ // the scratch machinery treats the parent stage as `Stage<Quad>`.
603
617
  const runScratchStage = async (chainedStage, stageDistribution) => {
604
618
  const scratchWriter = new FileWriter({
605
619
  outputDir: `${outputDir}/${chainedStage.name}`,
@@ -618,7 +632,7 @@ export class Pipeline {
618
632
  return scratchWriter.getOutputPath(dataset);
619
633
  };
620
634
  try {
621
- // 1. Run parent stage → scratch FileWriter.
635
+ // 1. Run parent stage → scratch FileWriter (quad-only, per above).
622
636
  const parentOutput = await runScratchStage(stage, distribution);
623
637
  outputFiles.push(parentOutput);
624
638
  // 2. Chain through children.
@@ -632,6 +646,8 @@ export class Pipeline {
632
646
  }
633
647
  // 3. Concatenate all output files → the open run, applying the plugins'
634
648
  // beforeStageWrite transforms once for the chain under the parent stage.
649
+ // The concatenation is quads (per the quad-only contract above), written
650
+ // to the `Out`-typed run writer – sound because chaining implies `Quad`.
635
651
  await this.stageWriter(runWriter, stage.name).write(dataset, this.readFiles(outputFiles));
636
652
  }
637
653
  finally {
package/dist/stage.d.ts CHANGED
@@ -26,6 +26,23 @@ export interface ReaderContext {
26
26
  distribution: Distribution;
27
27
  stage: string;
28
28
  }
29
+ /**
30
+ * The pipeline's one type-changing seam: turn a root-complete batch of quads
31
+ * into a stage's output items. The only extension point whose output is not
32
+ * `Quad` – a quad enrichment is a {@link QuadTransform} attached to a reader
33
+ * (extension point 1), which runs *before* this. See
34
+ * {@link https://github.com/ldelements/lde/blob/main/docs/decisions/0013-project-inside-the-batch-per-root-type.md | ADR 13}.
35
+ */
36
+ export type BatchTransform<Out> = (quads: readonly Quad[], context: BatchContext) => Iterable<Out> | AsyncIterable<Out>;
37
+ /**
38
+ * Context handed to a {@link BatchTransform}: {@link ReaderContext} plus the
39
+ * batch's selected roots. `bindings` are the item-selector rows the readers were
40
+ * given as a `VALUES` block, so the batch is root-complete by construction – the
41
+ * projection frames exactly these roots rather than discovering them.
42
+ */
43
+ export interface BatchContext extends ReaderContext {
44
+ bindings: readonly VariableBindings[];
45
+ }
29
46
  /**
30
47
  * An {@link Reader} with zero or more {@link QuadTransform}s attached as data.
31
48
  *
@@ -45,10 +62,27 @@ export interface AttachedReader {
45
62
  }
46
63
  /** One or more readers, each optionally carrying attached transforms. */
47
64
  export type StageReaders = Reader | AttachedReader | (Reader | AttachedReader)[];
48
- export interface StageOptions {
65
+ export interface StageOptions<Out = Quad> {
49
66
  name: string;
50
67
  readers: StageReaders;
51
68
  itemSelector?: ItemSelector;
69
+ /**
70
+ * Turn each root-complete batch into this stage's output items – the one seam
71
+ * whose output type differs from `Quad` ({@link BatchTransform}). Requires
72
+ * {@link StageOptions.itemSelector} (a batch only exists under a selector) and
73
+ * forbids {@link StageOptions.stages} (a chained stage serializes to
74
+ * N-Triples, which a projected item cannot). Omit for a plain quad stage.
75
+ */
76
+ project?: BatchTransform<Out>;
77
+ /**
78
+ * Capacity of the bounded queue funnelling this stage's concurrent batches
79
+ * into the single write. The queue applies backpressure at this many items,
80
+ * so it bounds memory – set it where the cost per item is large (a projected
81
+ * document is far heavier than a quad). Only meaningful with an item selector.
82
+ *
83
+ * @default 128
84
+ */
85
+ queueCapacity?: number;
52
86
  /**
53
87
  * Maximum number of bindings per reader call.
54
88
  *
@@ -74,7 +108,7 @@ export interface StageOptions {
74
108
  * Treat a supported stage that produces no quads as a hard failure (throws),
75
109
  * rather than a legitimately empty result.
76
110
  *
77
- * Set this for stages whose query must yield output — typically a scalar
111
+ * Set this for stages whose query must yield output – typically a scalar
78
112
  * aggregate such as `SELECT (COUNT(*) AS ?n)`, which always returns exactly
79
113
  * one row, so zero quads can only mean the endpoint truncated or aborted the
80
114
  * response (e.g. a timeout surfaced as an empty `HTTP 200`). The resulting
@@ -109,7 +143,7 @@ export interface SelectOptions {
109
143
  /** Per-call timeout policy. */
110
144
  timeout?: TimeoutPolicy;
111
145
  }
112
- export declare class Stage {
146
+ export declare class Stage<Out = Quad> {
113
147
  readonly name: string;
114
148
  readonly stages: readonly Stage[];
115
149
  /** Whether an empty result is treated as a hard failure. @see {@link StageOptions.expectsOutput} */
@@ -119,13 +153,15 @@ export declare class Stage {
119
153
  private readonly batchSize;
120
154
  private readonly maxConcurrency;
121
155
  private readonly validation?;
122
- constructor(options: StageOptions);
156
+ private readonly project?;
157
+ private readonly queueCapacity?;
158
+ constructor(options: StageOptions<Out>);
123
159
  /** The validator for this stage, if configured. */
124
160
  get validator(): Validator | undefined;
125
- run(dataset: Dataset, distribution: Distribution, writer: DatasetWriter, options?: RunOptions): Promise<NotSupported | void>;
161
+ run(dataset: Dataset, distribution: Distribution, writer: DatasetWriter<Out>, options?: RunOptions): Promise<NotSupported | void>;
126
162
  /**
127
163
  * Throw when {@link StageOptions.expectsOutput} is set but the stage produced
128
- * no quads — a supported-but-empty result that signals a truncated or aborted
164
+ * no quads – a supported-but-empty result that signals a truncated or aborted
129
165
  * endpoint response rather than a legitimately empty one.
130
166
  */
131
167
  private assertProduced;
@@ -1 +1 @@
1
- {"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"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"}
package/dist/stage.js CHANGED
@@ -11,7 +11,15 @@ export class Stage {
11
11
  batchSize;
12
12
  maxConcurrency;
13
13
  validation;
14
+ project;
15
+ queueCapacity;
14
16
  constructor(options) {
17
+ if (options.project && !options.itemSelector) {
18
+ throw new Error(`Stage '${options.name}': 'project' requires an 'itemSelector' – without one there is no batch to project, only the readers' whole output.`);
19
+ }
20
+ if (options.project && (options.stages?.length ?? 0) > 0) {
21
+ throw new Error(`Stage '${options.name}': 'project' cannot combine with chained 'stages' – a chained stage serializes to N-Triples, which a projected item cannot.`);
22
+ }
15
23
  this.name = options.name;
16
24
  this.stages = options.stages ?? [];
17
25
  this.readers = normalizeReaders(options.readers);
@@ -20,6 +28,8 @@ export class Stage {
20
28
  this.maxConcurrency = options.maxConcurrency ?? 10;
21
29
  this.validation = options.validation;
22
30
  this.expectsOutput = options.expectsOutput ?? false;
31
+ this.project = options.project;
32
+ this.queueCapacity = options.queueCapacity;
23
33
  }
24
34
  /** The validator for this stage, if configured. */
25
35
  get validator() {
@@ -36,6 +46,11 @@ export class Stage {
36
46
  if (streams instanceof NotSupported) {
37
47
  return streams;
38
48
  }
49
+ // The non-selector path has no batch, so `project` is forbidden here (the
50
+ // constructor rejects `project` without an `itemSelector`). It therefore
51
+ // only ever writes quads, and `Out` is `Quad` at runtime – tsc cannot see
52
+ // that invariant, so the writer is narrowed once.
53
+ const quadWriter = writer;
39
54
  // Quads the readers produced (before any validation filtering); used to
40
55
  // enforce `expectsOutput` below.
41
56
  let produced = 0;
@@ -50,7 +65,7 @@ export class Stage {
50
65
  const onInvalid = this.validation.onInvalid ?? 'write';
51
66
  if (onInvalid === 'write') {
52
67
  await Promise.all([
53
- writer.write(dataset, (async function* () {
68
+ quadWriter.write(dataset, (async function* () {
54
69
  yield* buffer;
55
70
  })()),
56
71
  this.validation.validator.validate(buffer, dataset),
@@ -59,7 +74,7 @@ export class Stage {
59
74
  else {
60
75
  const accepted = await this.validateBuffer(buffer, dataset);
61
76
  if (accepted.length > 0) {
62
- await writer.write(dataset, (async function* () {
77
+ await quadWriter.write(dataset, (async function* () {
63
78
  yield* accepted;
64
79
  })());
65
80
  }
@@ -68,18 +83,18 @@ export class Stage {
68
83
  else if (this.expectsOutput) {
69
84
  // Only thread the per-quad counter through when the count is actually
70
85
  // needed; the default path stays a plain streaming write with no overhead.
71
- await writer.write(dataset, countQuads(mergeStreams(streams), (count) => {
86
+ await quadWriter.write(dataset, countQuads(mergeStreams(streams), (count) => {
72
87
  produced = count;
73
88
  }));
74
89
  }
75
90
  else {
76
- await writer.write(dataset, mergeStreams(streams));
91
+ await quadWriter.write(dataset, mergeStreams(streams));
77
92
  }
78
93
  this.assertProduced(produced);
79
94
  }
80
95
  /**
81
96
  * Throw when {@link StageOptions.expectsOutput} is set but the stage produced
82
- * no quads — a supported-but-empty result that signals a truncated or aborted
97
+ * no quads – a supported-but-empty result that signals a truncated or aborted
83
98
  * endpoint response rather than a legitimately empty one.
84
99
  */
85
100
  assertProduced(produced) {
@@ -107,8 +122,11 @@ export class Stage {
107
122
  yield next.value;
108
123
  }
109
124
  })();
110
- const queue = new AsyncQueue();
125
+ const queue = new AsyncQueue(this.queueCapacity);
111
126
  let itemsProcessed = 0;
127
+ // Output items written this run. Equals quads written for a plain stage;
128
+ // for a projecting stage it counts projected items (documents), one or more
129
+ // – or fewer – per root, which is what `expectsOutput`/`onProgress` report.
112
130
  let quadsGenerated = 0;
113
131
  let hasResults = false;
114
132
  const onInvalid = this.validation?.onInvalid ?? 'write';
@@ -157,25 +175,34 @@ export class Stage {
157
175
  return quads;
158
176
  }));
159
177
  const batchQuads = readerOutputs.flat();
178
+ // Validation runs on the quads, before projection – validators are
179
+ // quad-typed. 'skip'/'halt' must resolve it before writing; 'write'
180
+ // validates concurrently, below, without blocking.
181
+ let acceptedQuads = batchQuads;
160
182
  if (this.validation &&
161
183
  batchQuads.length > 0 &&
162
184
  onInvalid !== 'write') {
163
- // 'skip' or 'halt': must await validation before deciding to write.
164
- const accepted = await this.validateBuffer(batchQuads, dataset);
165
- for (const quad of accepted) {
166
- await queue.push(quad);
167
- quadsGenerated++;
168
- }
185
+ acceptedQuads = await this.validateBuffer(batchQuads, dataset);
169
186
  }
170
- else {
171
- for (const quad of batchQuads) {
172
- await queue.push(quad);
173
- quadsGenerated++;
174
- }
175
- if (this.validation && batchQuads.length > 0) {
176
- // 'write' mode: validate concurrently without blocking the write path.
177
- pendingValidations.push(this.validation.validator.validate(batchQuads, dataset));
178
- }
187
+ // The one type-changing seam: project the root-complete batch into
188
+ // output items, or pass the quads through for a plain stage (`Out`
189
+ // is `Quad` at runtime when there is no projection).
190
+ const items = this.project
191
+ ? this.project(acceptedQuads, {
192
+ dataset,
193
+ distribution,
194
+ stage: this.name,
195
+ bindings,
196
+ })
197
+ : acceptedQuads;
198
+ for await (const item of items) {
199
+ await queue.push(item);
200
+ quadsGenerated++;
201
+ }
202
+ if (this.validation &&
203
+ batchQuads.length > 0 &&
204
+ onInvalid === 'write') {
205
+ pendingValidations.push(this.validation.validator.validate(batchQuads, dataset));
179
206
  }
180
207
  itemsProcessed += bindings.length;
181
208
  options?.onProgress?.(itemsProcessed, quadsGenerated);
@@ -291,7 +318,7 @@ async function* mergeStreams(streams) {
291
318
  * via `onCount` once the stream is exhausted. Lets a streaming write enforce
292
319
  * {@link StageOptions.expectsOutput} without buffering.
293
320
  *
294
- * `onCount` fires only when the consumer drains the stream — which the pipeline
321
+ * `onCount` fires only when the consumer drains the stream – which the pipeline
295
322
  * writers do. A writer that stops early would leave the count short; callers
296
323
  * relying on it for `expectsOutput` must consume the stream fully.
297
324
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/pipeline",
3
- "version": "0.34.1",
3
+ "version": "0.34.3",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/pipeline"
@@ -26,14 +26,14 @@
26
26
  "dependencies": {
27
27
  "@lde/dataset": "^0.7.8",
28
28
  "@lde/dataset-registry-client": "^0.8.6",
29
- "@lde/distribution-health": "^0.2.6",
29
+ "@lde/distribution-health": "^0.2.7",
30
30
  "@lde/distribution-probe": "^0.2.6",
31
- "@lde/sparql-importer": "^0.6.6",
31
+ "@lde/sparql-importer": "^0.6.7",
32
32
  "@lde/sparql-server": "^0.4.12",
33
33
  "@rdfjs/namespace": "^2.0.1",
34
34
  "@rdfjs/types": "^2.0.1",
35
35
  "@tpluscode/rdf-ns-builders": "^5.0.0",
36
- "@traqula/generator-sparql-1-1": "^1.1.6",
36
+ "@traqula/generator-sparql-1-1": "^1.1.8",
37
37
  "@traqula/parser-sparql-1-1": "^1.1.5",
38
38
  "@traqula/rules-sparql-1-1": "^1.1.0",
39
39
  "fetch-sparql-endpoint": "^7.1.1",