@lde/pipeline 0.31.5 → 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.
@@ -1,6 +1,5 @@
1
1
  import { Dataset } from '@lde/dataset';
2
- import type { Quad } from '@rdfjs/types';
3
- import { Writer } from './writer.js';
2
+ import { RunContext, RunWriter, Writer } from './writer.js';
4
3
  export interface FileWriterOptions {
5
4
  /**
6
5
  * Output directory for written files.
@@ -32,21 +31,47 @@ export interface FileWriterOptions {
32
31
  */
33
32
  graphIri?: (dataset: Dataset) => URL;
34
33
  }
34
+ /**
35
+ * The run writer a {@link FileWriter} opens: per-dataset `flush` and `reset`
36
+ * are always available, so direct callers need no optional chaining.
37
+ */
38
+ export interface FileRunWriter extends RunWriter {
39
+ flush(dataset: Dataset): Promise<void>;
40
+ reset(dataset: Dataset): Promise<void>;
41
+ }
42
+ /**
43
+ * Streams RDF quads to files on disk using N3 Writer.
44
+ *
45
+ * Files are named based on the dataset IRI using filenamify-url.
46
+ *
47
+ * Within a run ({@link openRun}), a single N3Writer is kept open per dataset
48
+ * across all `write` calls, so Turtle prefix declarations are written once and
49
+ * triples can be grouped by subject. `flush` finalizes a dataset's file;
50
+ * `commit` finalizes any files still open, and `abort` discards their
51
+ * temporary output, leaving no half-written final file behind.
52
+ */
35
53
  export declare class FileWriter implements Writer {
36
54
  private readonly outputDir;
37
55
  readonly format: 'turtle' | 'n-triples' | 'n-quads';
38
56
  private readonly replacementCharacter;
39
57
  private readonly prefixes?;
40
58
  private readonly graphIri?;
41
- private readonly activeWriters;
42
59
  constructor(options: FileWriterOptions);
43
- write(dataset: Dataset, quads: AsyncIterable<Quad>): Promise<void>;
44
- flush(dataset: Dataset): Promise<void>;
45
- reset(dataset: Dataset): Promise<void>;
60
+ openRun(_context?: RunContext): Promise<FileRunWriter>;
61
+ private writeQuads;
62
+ private flushFile;
63
+ private flushPath;
64
+ private discardFile;
65
+ /**
66
+ * Close an open file's stream and remove its temp file. Await the stream
67
+ * closing before removing: the write stream opens its fd lazily, so a
68
+ * pending open could otherwise recreate the file after rm() ran.
69
+ */
70
+ private closeAndRemove;
46
71
  getOutputPath(dataset: Dataset): string;
47
72
  getFilename(dataset: Dataset): string;
48
73
  private getFilePath;
49
- private getOrCreateWriter;
74
+ private getOrCreateFile;
50
75
  private getExtension;
51
76
  }
52
77
  //# sourceMappingURL=fileWriter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileWriter.d.ts","sourceRoot":"","sources":["../../src/writer/fileWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAMzC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,GAAG,CAAC;CACtC;AAiBD,qBAAa,UAAW,YAAW,MAAM;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;IACpD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA4B;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAG1B;gBAEQ,OAAO,EAAE,iBAAiB;IAQhC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAiClE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BtC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB5C,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM;IAIvC,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM;IAQrC,OAAO,CAAC,WAAW;YAIL,iBAAiB;IA4B/B,OAAO,CAAC,YAAY;CAUrB"}
1
+ {"version":3,"file":"fileWriter.d.ts","sourceRoot":"","sources":["../../src/writer/fileWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAOvC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,GAAG,CAAC;CACtC;AAgBD;;;GAGG;AACH,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED;;;;;;;;;;GAUG;AACH,qBAAa,UAAW,YAAW,MAAM;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;IACpD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA4B;gBAE1C,OAAO,EAAE,iBAAiB;IAQhC,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;YAwB9C,UAAU;YAqCV,SAAS;YAOT,SAAS;YAgCT,WAAW;IAczB;;;;OAIG;YACW,cAAc;IAQ5B,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM;IAIvC,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM;IAQrC,OAAO,CAAC,WAAW;YAIL,eAAe;IA6B7B,OAAO,CAAC,YAAY;CAUrB"}
@@ -3,27 +3,28 @@ import { mkdir, rename, rm } from 'node:fs/promises';
3
3
  import { join, dirname } from 'node:path';
4
4
  import filenamifyUrl from 'filenamify-url';
5
5
  import { DataFactory, Writer as N3Writer } from 'n3';
6
+ const formatMap = {
7
+ turtle: 'Turtle',
8
+ 'n-triples': 'N-Triples',
9
+ 'n-quads': 'N-Quads',
10
+ };
6
11
  /**
7
12
  * Streams RDF quads to files on disk using N3 Writer.
8
13
  *
9
14
  * Files are named based on the dataset IRI using filenamify-url.
10
15
  *
11
- * A single N3Writer is kept open per dataset across all {@link write} calls,
12
- * so Turtle prefix declarations are written once and triples can be grouped
13
- * by subject. Call {@link flush} after all stages complete to finalize the file.
16
+ * Within a run ({@link openRun}), a single N3Writer is kept open per dataset
17
+ * across all `write` calls, so Turtle prefix declarations are written once and
18
+ * triples can be grouped by subject. `flush` finalizes a dataset's file;
19
+ * `commit` finalizes any files still open, and `abort` discards their
20
+ * temporary output, leaving no half-written final file behind.
14
21
  */
15
- const formatMap = {
16
- turtle: 'Turtle',
17
- 'n-triples': 'N-Triples',
18
- 'n-quads': 'N-Quads',
19
- };
20
22
  export class FileWriter {
21
23
  outputDir;
22
24
  format;
23
25
  replacementCharacter;
24
26
  prefixes;
25
27
  graphIri;
26
- activeWriters = new Map();
27
28
  constructor(options) {
28
29
  this.outputDir = options.outputDir;
29
30
  this.format = options.format ?? 'n-triples';
@@ -31,13 +32,36 @@ export class FileWriter {
31
32
  this.prefixes = options.prefixes;
32
33
  this.graphIri = options.graphIri;
33
34
  }
34
- async write(dataset, quads) {
35
+ async openRun(_context) {
36
+ const openFiles = new Map();
37
+ return {
38
+ write: (dataset, quads) => this.writeQuads(openFiles, dataset, quads),
39
+ flush: (dataset) => this.flushFile(openFiles, dataset),
40
+ reset: (dataset) => this.discardFile(openFiles, dataset),
41
+ commit: async () => {
42
+ // Finalize files not yet flushed per dataset – a safety net so a
43
+ // committed run never leaves a complete output stuck in a temp file.
44
+ for (const { finalPath } of openFiles.values()) {
45
+ await this.flushPath(openFiles, finalPath);
46
+ }
47
+ },
48
+ abort: async () => {
49
+ // Discard all temp output: a crash or abort leaves at most a stale
50
+ // `*.tmp`, never a truncated final file.
51
+ for (const openFile of openFiles.values()) {
52
+ await this.closeAndRemove(openFile);
53
+ }
54
+ openFiles.clear();
55
+ },
56
+ };
57
+ }
58
+ async writeQuads(openFiles, dataset, quads) {
35
59
  // Peek at the first quad to avoid creating empty files.
36
60
  const iterator = quads[Symbol.asyncIterator]();
37
61
  const first = await iterator.next();
38
62
  if (first.done)
39
63
  return;
40
- const { n3Writer } = await this.getOrCreateWriter(dataset);
64
+ const { n3Writer } = await this.getOrCreateFile(openFiles, dataset);
41
65
  // Re-emit each quad into the configured named graph (n-quads only). The
42
66
  // pipeline's quads carry no graph context, so the graph is supplied here
43
67
  // exactly as SparqlUpdateWriter supplies it via INSERT DATA { GRAPH … }.
@@ -52,23 +76,25 @@ export class FileWriter {
52
76
  addQuad(quad);
53
77
  }
54
78
  }
55
- async flush(dataset) {
56
- const key = this.getFilePath(dataset);
57
- const entry = this.activeWriters.get(key);
58
- if (!entry)
79
+ async flushFile(openFiles, dataset) {
80
+ await this.flushPath(openFiles, this.getFilePath(dataset));
81
+ }
82
+ async flushPath(openFiles, finalPath) {
83
+ const openFile = openFiles.get(finalPath);
84
+ if (!openFile)
59
85
  return;
60
- this.activeWriters.delete(key);
86
+ openFiles.delete(finalPath);
61
87
  // Quads are streamed to a sibling temp file; only on a clean flush is it
62
88
  // atomically renamed onto the final path. A crash therefore leaves at most
63
89
  // a stale `*.tmp` — never a truncated final file — so a downstream index
64
90
  // rebuild that globs the final extension never reads a half-written file.
65
91
  try {
66
92
  await new Promise((resolve, reject) => {
67
- if (entry.stream.errored) {
68
- reject(entry.stream.errored);
93
+ if (openFile.stream.errored) {
94
+ reject(openFile.stream.errored);
69
95
  return;
70
96
  }
71
- entry.n3Writer.end((error) => {
97
+ openFile.n3Writer.end((error) => {
72
98
  if (error)
73
99
  reject(error);
74
100
  else
@@ -77,26 +103,32 @@ export class FileWriter {
77
103
  });
78
104
  }
79
105
  catch (error) {
80
- await rm(entry.tempPath, { force: true, recursive: true });
106
+ await rm(openFile.tempPath, { force: true, recursive: true });
81
107
  throw error;
82
108
  }
83
- await rename(entry.tempPath, key);
109
+ await rename(openFile.tempPath, finalPath);
84
110
  }
85
- async reset(dataset) {
111
+ async discardFile(openFiles, dataset) {
86
112
  const key = this.getFilePath(dataset);
87
- const entry = this.activeWriters.get(key);
88
- if (!entry)
113
+ const openFile = openFiles.get(key);
114
+ if (!openFile)
89
115
  return;
90
116
  // Drop the open writer and remove its temp file so the next write starts a
91
- // fresh file, discarding everything streamed during the previous pass. Await
92
- // the stream closing before removing: the write stream opens its fd lazily,
93
- // so a pending open could otherwise recreate the file after rm() ran.
94
- this.activeWriters.delete(key);
117
+ // fresh file, discarding everything streamed during the previous pass.
118
+ openFiles.delete(key);
119
+ await this.closeAndRemove(openFile);
120
+ }
121
+ /**
122
+ * Close an open file's stream and remove its temp file. Await the stream
123
+ * closing before removing: the write stream opens its fd lazily, so a
124
+ * pending open could otherwise recreate the file after rm() ran.
125
+ */
126
+ async closeAndRemove(openFile) {
95
127
  await new Promise((resolve) => {
96
- entry.stream.once('close', resolve);
97
- entry.stream.destroy();
128
+ openFile.stream.once('close', resolve);
129
+ openFile.stream.destroy();
98
130
  });
99
- await rm(entry.tempPath, { force: true, recursive: true });
131
+ await rm(openFile.tempPath, { force: true, recursive: true });
100
132
  }
101
133
  getOutputPath(dataset) {
102
134
  return this.getFilePath(dataset);
@@ -111,16 +143,16 @@ export class FileWriter {
111
143
  getFilePath(dataset) {
112
144
  return join(this.outputDir, this.getFilename(dataset));
113
145
  }
114
- async getOrCreateWriter(dataset) {
115
- const key = this.getFilePath(dataset);
116
- const existing = this.activeWriters.get(key);
146
+ async getOrCreateFile(openFiles, dataset) {
147
+ const finalPath = this.getFilePath(dataset);
148
+ const existing = openFiles.get(finalPath);
117
149
  if (existing)
118
150
  return existing;
119
- await mkdir(dirname(key), { recursive: true });
151
+ await mkdir(dirname(finalPath), { recursive: true });
120
152
  // Write to a sibling temp file (same directory, so the flush rename stays on
121
153
  // one filesystem and is atomic). The `.tmp` suffix keeps it out of any glob
122
154
  // on the final extension.
123
- const tempPath = `${key}.tmp`;
155
+ const tempPath = `${finalPath}.tmp`;
124
156
  const stream = createWriteStream(tempPath, { flags: 'w' });
125
157
  stream.on('error', (error) => {
126
158
  // Surface stream errors when flushing; prevents 'unhandled error' crashes.
@@ -130,9 +162,9 @@ export class FileWriter {
130
162
  format: formatMap[this.format],
131
163
  prefixes: this.prefixes,
132
164
  });
133
- const entry = { n3Writer, stream, tempPath };
134
- this.activeWriters.set(key, entry);
135
- return entry;
165
+ const openFile = { n3Writer, stream, tempPath, finalPath };
166
+ openFiles.set(finalPath, openFile);
167
+ return openFile;
136
168
  }
137
169
  getExtension() {
138
170
  switch (this.format) {
@@ -1,6 +1,5 @@
1
1
  import { Dataset } from '@lde/dataset';
2
- import type { Quad } from '@rdfjs/types';
3
- import { Writer } from './writer.js';
2
+ import { RunContext, RunWriter, Writer } from './writer.js';
4
3
  export interface SparqlWriterOptions {
5
4
  /**
6
5
  * The SPARQL UPDATE endpoint URL.
@@ -34,12 +33,25 @@ export interface SparqlWriterOptions {
34
33
  */
35
34
  graphIri?: (dataset: Dataset) => URL;
36
35
  }
36
+ /**
37
+ * The run writer a {@link SparqlUpdateWriter} opens: per-dataset `reset` is
38
+ * always available, so direct callers need no optional chaining.
39
+ */
40
+ export interface SparqlUpdateRunWriter extends RunWriter {
41
+ reset(dataset: Dataset): Promise<void>;
42
+ }
37
43
  /**
38
44
  * Writes RDF data to a SPARQL endpoint using SPARQL UPDATE INSERT DATA queries.
39
45
  *
40
- * Clears the named graph before the first write per dataset per instance, then
41
- * streams quads in batches to avoid accumulating the entire dataset in memory.
42
- * Subsequent calls to {@link write} for the same dataset append rather than replace.
46
+ * Within a run ({@link openRun}), the named graph is cleared before the first
47
+ * write per dataset, then quads are streamed in batches to avoid accumulating
48
+ * the entire dataset in memory. Subsequent writes for the same dataset within
49
+ * the run append rather than replace. Each new run starts with fresh
50
+ * cleared-graph state, so re-running a pipeline replaces each graph again.
51
+ *
52
+ * The writes are visible as they land (no run-level staging), so `commit` and
53
+ * `abort` are no-ops: an aborted run leaves the graphs written so far, to be
54
+ * replaced by the next run.
43
55
  */
44
56
  export declare class SparqlUpdateWriter implements Writer {
45
57
  private readonly endpoint;
@@ -47,10 +59,9 @@ export declare class SparqlUpdateWriter implements Writer {
47
59
  private readonly fetch;
48
60
  private readonly batchSize;
49
61
  private readonly graphIri;
50
- private readonly clearedGraphs;
51
62
  constructor(options: SparqlWriterOptions);
52
- write(dataset: Dataset, quads: AsyncIterable<Quad>): Promise<void>;
53
- reset(dataset: Dataset): Promise<void>;
63
+ openRun(_context?: RunContext): Promise<SparqlUpdateRunWriter>;
64
+ private writeQuads;
54
65
  private clearGraph;
55
66
  private insertBatch;
56
67
  private executeUpdate;
@@ -1 +1 @@
1
- {"version":3,"file":"sparqlUpdateWriter.d.ts","sourceRoot":"","sources":["../../src/writer/sparqlUpdateWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAiB,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,QAAQ,EAAE,GAAG,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,GAAG,CAAC;CACtC;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,MAAM;IAC/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAM;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;gBAEvC,OAAO,EAAE,mBAAmB;IAQlC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAclE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAM9B,UAAU;YAIV,WAAW;YAOX,aAAa;CAuB5B"}
1
+ {"version":3,"file":"sparqlUpdateWriter.d.ts","sourceRoot":"","sources":["../../src/writer/sparqlUpdateWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAiB,MAAM,cAAc,CAAC;AAGtD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG5D,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,QAAQ,EAAE,GAAG,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,GAAG,CAAC;CACtC;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAsB,SAAQ,SAAS;IACtD,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,kBAAmB,YAAW,MAAM;IAC/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAM;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;gBAEzC,OAAO,EAAE,mBAAmB;IAQlC,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,qBAAqB,CAAC;YActD,UAAU;YAkBV,UAAU;YAIV,WAAW;YAOX,aAAa;CAuB5B"}
@@ -4,9 +4,15 @@ import { serializeQuads } from './serialize.js';
4
4
  /**
5
5
  * Writes RDF data to a SPARQL endpoint using SPARQL UPDATE INSERT DATA queries.
6
6
  *
7
- * Clears the named graph before the first write per dataset per instance, then
8
- * streams quads in batches to avoid accumulating the entire dataset in memory.
9
- * Subsequent calls to {@link write} for the same dataset append rather than replace.
7
+ * Within a run ({@link openRun}), the named graph is cleared before the first
8
+ * write per dataset, then quads are streamed in batches to avoid accumulating
9
+ * the entire dataset in memory. Subsequent writes for the same dataset within
10
+ * the run append rather than replace. Each new run starts with fresh
11
+ * cleared-graph state, so re-running a pipeline replaces each graph again.
12
+ *
13
+ * The writes are visible as they land (no run-level staging), so `commit` and
14
+ * `abort` are no-ops: an aborted run leaves the graphs written so far, to be
15
+ * replaced by the next run.
10
16
  */
11
17
  export class SparqlUpdateWriter {
12
18
  endpoint;
@@ -14,7 +20,6 @@ export class SparqlUpdateWriter {
14
20
  fetch;
15
21
  batchSize;
16
22
  graphIri;
17
- clearedGraphs = new Set();
18
23
  constructor(options) {
19
24
  this.endpoint = options.endpoint;
20
25
  this.auth = options.auth;
@@ -22,22 +27,30 @@ export class SparqlUpdateWriter {
22
27
  this.batchSize = options.batchSize ?? 10000;
23
28
  this.graphIri = options.graphIri ?? ((dataset) => dataset.iri);
24
29
  }
25
- async write(dataset, quads) {
30
+ async openRun(_context) {
31
+ const clearedGraphs = new Set();
32
+ return {
33
+ write: (dataset, quads) => this.writeQuads(clearedGraphs, dataset, quads),
34
+ reset: async (dataset) => {
35
+ // Forget the graph’s cleared state so the next write re-issues
36
+ // CLEAR GRAPH, replacing the prior output instead of appending to it.
37
+ clearedGraphs.delete(this.graphIri(dataset).toString());
38
+ },
39
+ commit: () => Promise.resolve(),
40
+ abort: () => Promise.resolve(),
41
+ };
42
+ }
43
+ async writeQuads(clearedGraphs, dataset, quads) {
26
44
  const graphUri = this.graphIri(dataset).toString();
27
45
  assertSafeIri(graphUri);
28
- if (!this.clearedGraphs.has(graphUri)) {
46
+ if (!clearedGraphs.has(graphUri)) {
29
47
  await this.clearGraph(graphUri);
30
- this.clearedGraphs.add(graphUri);
48
+ clearedGraphs.add(graphUri);
31
49
  }
32
50
  for await (const chunk of batch(quads, this.batchSize)) {
33
51
  await this.insertBatch(graphUri, chunk);
34
52
  }
35
53
  }
36
- async reset(dataset) {
37
- // Forget the graph’s cleared state so the next write re-issues CLEAR GRAPH,
38
- // replacing the prior output instead of appending to it.
39
- this.clearedGraphs.delete(this.graphIri(dataset).toString());
40
- }
41
54
  async clearGraph(graphUri) {
42
55
  await this.executeUpdate(`CLEAR GRAPH <${graphUri}>`);
43
56
  }
@@ -1,35 +1,113 @@
1
1
  import { Dataset } from '@lde/dataset';
2
2
  import type { Quad } from '@rdfjs/types';
3
+ import type { ProvenanceStore } from '../provenance/store.js';
3
4
  /**
4
- * Interface for writing RDF data to a destination.
5
+ * Context for one pipeline run, handed to {@link Writer.openRun}.
5
6
  */
6
- export interface Writer {
7
+ export interface RunContext {
7
8
  /**
8
- * Write RDF data for a dataset to the destination.
9
+ * Unique identifier of this run. An In-place writer stamps written documents
10
+ * with it so its commit sweep can delete what the run did not touch; a
11
+ * Blue/green writer may use it to name the fresh collection it builds.
12
+ */
13
+ readonly runId: string;
14
+ /**
15
+ * ISO 8601 timestamp taken when the run opened. Injected so writers need no
16
+ * clock of their own.
17
+ */
18
+ readonly startedAt: string;
19
+ /**
20
+ * IRIs of every dataset the selector produced this run – including datasets
21
+ * the pipeline skipped as unchanged. Grows while the pipeline iterates the
22
+ * selection; it is complete by the time {@link RunWriter.commit} runs, which
23
+ * is when a registry-membership sweep should read it.
24
+ */
25
+ selectedSources(): Iterable<string>;
26
+ /**
27
+ * The pipeline’s per-dataset processing memory, present when skip-unchanged
28
+ * is enabled ({@link PipelineOptions.provenanceStore}).
29
+ */
30
+ readonly provenance?: ProvenanceStore;
31
+ }
32
+ /**
33
+ * The per-dataset write surface of a {@link RunWriter}: what a stage writes
34
+ * its output through. Separate from the run lifecycle so stages can write but
35
+ * never commit or abort the run.
36
+ */
37
+ export interface DatasetWriter<Item = Quad> {
38
+ /**
39
+ * Write a dataset’s items to the destination. Called one or more times per
40
+ * dataset (once per stage) within an open run.
9
41
  *
10
42
  * @param dataset The dataset metadata
11
- * @param quads The RDF quads to write
43
+ * @param items The items to write
12
44
  */
13
- write(dataset: Dataset, quads: AsyncIterable<Quad>): Promise<void>;
45
+ write(dataset: Dataset, items: AsyncIterable<Item>): Promise<void>;
46
+ }
47
+ /**
48
+ * One open run transaction on a destination: per-dataset writes bracketed by
49
+ * exactly one {@link commit} or {@link abort}. Obtained from
50
+ * {@link Writer.openRun}; the pipeline drives
51
+ * `openRun → write* → commit/abort` uniformly and never branches on the
52
+ * writer’s update mode – an atomic swap or a deletion sweep is the writer’s
53
+ * own business, inside {@link commit}.
54
+ */
55
+ export interface RunWriter<Item = Quad> extends DatasetWriter<Item> {
14
56
  /**
15
- * Finalize writing for a dataset. Called after all stages complete.
57
+ * Finalize writing for a dataset. Called after all stages complete for that
58
+ * dataset.
16
59
  *
17
60
  * Writers that buffer output across multiple {@link write} calls (e.g. to
18
61
  * share Turtle prefix declarations) should implement this to flush remaining
19
- * data and release resources.
62
+ * data and release per-dataset resources.
20
63
  */
21
64
  flush?(dataset: Dataset): Promise<void>;
22
65
  /**
23
- * Discard a dataset’s already-written output so a subsequent run starts from
24
- * a clean slate. Called by the pipeline before it re-runs all stages against
25
- * a fallback source (an imported data dump), so endpoint-sourced partial
26
- * results are not mixed with the dump-sourced re-run.
66
+ * Discard a dataset’s already-written output so a subsequent pass starts
67
+ * from a clean slate. Called by the pipeline before it re-runs all stages
68
+ * against a fallback source (an imported data dump), so endpoint-sourced
69
+ * partial results are not mixed with the dump-sourced re-run.
27
70
  *
28
- * Writers that build a complete replacement per dataset (e.g.
29
- * {@link SparqlUpdateWriter}, which clears each graph on first write) should
30
- * implement this to reset that per-dataset state. Writers without
31
- * replaceable output may omit it; the re-run then appends.
71
+ * Writers that build a complete replacement per dataset should implement
72
+ * this to reset that per-dataset state. Writers without replaceable output
73
+ * may omit it; the re-run then appends.
32
74
  */
33
75
  reset?(dataset: Dataset): Promise<void>;
76
+ /**
77
+ * Finalize the run: the one place a destination goes live. A Blue/green
78
+ * writer atomically swaps its alias to the freshly built collection; an
79
+ * In-place writer sweeps documents the run did not touch and releases its
80
+ * lock. Called exactly once, after every dataset has been processed.
81
+ *
82
+ * The driver flushes every written dataset before committing (the Pipeline
83
+ * does), so unflushed writes only reach `commit` under direct use; a writer
84
+ * that can encounter them should finalize them non-destructively.
85
+ */
86
+ commit(): Promise<void>;
87
+ /**
88
+ * Abandon the run after a failure: the live destination must be left as it
89
+ * was before the run. A Blue/green writer drops its half-built collection;
90
+ * an In-place writer releases its lock and lets the next run reconcile.
91
+ *
92
+ * @param error The failure that ended the run
93
+ */
94
+ abort(error: unknown): Promise<void>;
95
+ }
96
+ /**
97
+ * A destination for pipeline output: a factory of per-run transactions.
98
+ *
99
+ * Each pipeline run opens one {@link RunWriter} via {@link openRun} – the
100
+ * home of run-level lifecycle such as alias swaps, deletion sweeps and
101
+ * cross-pod locks. A destination without run-level state simply returns its
102
+ * per-dataset writes with no-op `commit`/`abort` (see
103
+ * {@link SparqlUpdateWriter} for the pattern).
104
+ */
105
+ export interface Writer<Item = Quad> {
106
+ /**
107
+ * Open a run transaction against this destination.
108
+ *
109
+ * @param context The run’s identity and selection scope
110
+ */
111
+ openRun(context: RunContext): Promise<RunWriter<Item>>;
34
112
  }
35
113
  //# sourceMappingURL=writer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/writer/writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;;;;;OAMG;IACH,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;;;;;;;OAUG;IACH,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC"}
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/writer/writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,eAAe,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;CACvC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa,CAAC,IAAI,GAAG,IAAI;IACxC;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS,CAAC,IAAI,GAAG,IAAI,CAAE,SAAQ,aAAa,CAAC,IAAI,CAAC;IACjE;;;;;;;OAOG;IACH,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;;;;;;OASG;IACH,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;;;;;;OASG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,MAAM,CAAC,IAAI,GAAG,IAAI;IACjC;;;;OAIG;IACH,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;CACxD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/pipeline",
3
- "version": "0.31.5",
3
+ "version": "0.32.0",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/pipeline"
@@ -24,12 +24,12 @@
24
24
  "!**/*.tsbuildinfo"
25
25
  ],
26
26
  "dependencies": {
27
- "@lde/dataset": "^0.7.7",
28
- "@lde/dataset-registry-client": "^0.8.4",
29
- "@lde/distribution-health": "^0.2.2",
30
- "@lde/distribution-probe": "^0.2.2",
31
- "@lde/sparql-importer": "^0.6.5",
32
- "@lde/sparql-server": "^0.4.11",
27
+ "@lde/dataset": "^0.7.8",
28
+ "@lde/dataset-registry-client": "^0.8.5",
29
+ "@lde/distribution-health": "^0.2.3",
30
+ "@lde/distribution-probe": "^0.2.3",
31
+ "@lde/sparql-importer": "^0.6.6",
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",
@@ -1 +0,0 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/sparql/executor.ts"],"names":[],"mappings":"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"}