n3.js-messages 0.1.2 → 0.2.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 +73 -8
- package/browser/THIRD-PARTY-LICENSES.txt +172 -0
- package/browser/index.global.js +27 -0
- package/browser/index.global.js.LEGAL.txt +15 -0
- package/browser/index.global.js.map +7 -0
- package/browser/index.js +27 -0
- package/browser/index.js.LEGAL.txt +15 -0
- package/browser/index.js.map +7 -0
- package/browser.d.ts +26 -0
- package/common.d.ts +89 -0
- package/index.d.ts +2 -76
- package/lib/MessageParser.js +38 -4
- package/lib/MessageParser.js.map +1 -0
- package/lib/MessageParserCore.js +42 -6
- package/lib/MessageParserCore.js.map +1 -0
- package/lib/MessageStreamParser.js +12 -1
- package/lib/MessageStreamParser.js.map +1 -0
- package/lib/MessageStreamWriter.js +13 -3
- package/lib/MessageStreamWriter.js.map +1 -0
- package/lib/MessageWriter.js +141 -18
- package/lib/MessageWriter.js.map +1 -0
- package/lib/formats.js +7 -2
- package/lib/formats.js.map +1 -0
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -0
- package/package.json +42 -14
- package/src/MessageParser.js +35 -3
- package/src/MessageParserCore.js +34 -5
- package/src/MessageStreamParser.js +8 -0
- package/src/MessageStreamWriter.js +10 -2
- package/src/MessageWriter.js +138 -20
- package/src/browser.js +61 -0
- package/src/formats.js +5 -1
- package/src/internal.d.ts +15 -0
- package/lib/MessageScanner.js +0 -395
package/src/MessageParserCore.js
CHANGED
|
@@ -5,29 +5,42 @@ import { EventEmitter } from 'events';
|
|
|
5
5
|
import { Parser, DataFactory } from 'n3';
|
|
6
6
|
import { resolveFormat, isMessagesVersion, baseVersion, isSupportedVersion } from './formats.js';
|
|
7
7
|
|
|
8
|
+
/** @typedef {import('@rdfjs/types').Term} BaseTerm */
|
|
9
|
+
/** @template {import('@rdfjs/types').BaseQuad} [Q=import('@rdfjs/types').Quad] */
|
|
8
10
|
export default class MessageParserCore {
|
|
9
11
|
// `sink` receives `message(quads, index)`, `end(prefixes)`, `error(error)`
|
|
10
12
|
// and optionally `quad(quad, messageIndex)`, `prefix(prefix, term)`, `version(label)`
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('../common.js').MessageParserOptions<Q> | undefined} options
|
|
15
|
+
* @param {import('./internal.js').ParserSink<Q>} sink
|
|
16
|
+
*/
|
|
11
17
|
constructor(options, sink) {
|
|
12
18
|
options = options || {};
|
|
13
19
|
const format = resolveFormat(options.format);
|
|
14
20
|
this._sink = sink;
|
|
15
|
-
|
|
21
|
+
|
|
22
|
+
/** @type {import('../common.js').MessageQuadFactory<Q>} */
|
|
23
|
+
this._factory = options.factory || /** @type {import('../common.js').MessageQuadFactory<Q>} */ (DataFactory);
|
|
16
24
|
this._blankNodePrefix = typeof options.blankNodePrefix === 'string' ?
|
|
17
25
|
options.blankNodePrefix.replace(/^_:/, '') : undefined;
|
|
18
26
|
this._blankNodeCounter = 0;
|
|
19
27
|
this._supportedVersions = Parser.SUPPORTED_VERSIONS;
|
|
20
28
|
this._messages = options.messages === true || isMessagesVersion(options.version);
|
|
21
29
|
this._index = 0;
|
|
30
|
+
|
|
31
|
+
/** @type {Q[] | null} */
|
|
22
32
|
this._current = null;
|
|
33
|
+
|
|
34
|
+
/** @type {Map<string, import('@rdfjs/types').BlankNode> | null} */
|
|
23
35
|
this._scope = null;
|
|
24
36
|
this._seenQuad = false;
|
|
25
37
|
this._failed = false;
|
|
26
38
|
this._finished = false;
|
|
27
39
|
|
|
40
|
+
/** @type {import('n3').ParserOptions} */
|
|
28
41
|
const n3Options = {
|
|
29
42
|
format: format.name,
|
|
30
|
-
factory: this._factory,
|
|
43
|
+
factory: /** @type {import('@rdfjs/types').DataFactory} */ (/** @type {unknown} */ (this._factory)),
|
|
31
44
|
// Version labels are validated here, because N3.js does not know `-messages` labels
|
|
32
45
|
parseUnsupportedVersions: true,
|
|
33
46
|
// N3.js reports `MESSAGE` and `@message .` between statements through `onDirective`,
|
|
@@ -47,7 +60,7 @@ export default class MessageParserCore {
|
|
|
47
60
|
this._line = 0;
|
|
48
61
|
this._delimiterLine = 0;
|
|
49
62
|
new Parser(n3Options).parse(this._input, {
|
|
50
|
-
onQuad: (error, quad, prefixes) => this._onQuad(error, quad, prefixes),
|
|
63
|
+
onQuad: (error, quad, prefixes) => this._onQuad(error, /** @type {Q | null} */ (quad), prefixes),
|
|
51
64
|
onPrefix: (prefix, term) => this._failed || sink.prefix && sink.prefix(prefix, term),
|
|
52
65
|
onVersion: label => this._failed || this._announce(label),
|
|
53
66
|
onDirective: () => this._failed || this._delimiter(),
|
|
@@ -56,6 +69,7 @@ export default class MessageParserCore {
|
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
// ### `write` accepts the next chunk of text
|
|
72
|
+
/** @param {string} text */
|
|
59
73
|
write(text) {
|
|
60
74
|
if (!this._failed && !this._finished && text.length > 0)
|
|
61
75
|
this._input.emit('data', text);
|
|
@@ -68,6 +82,7 @@ export default class MessageParserCore {
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
// Keeps track of lines; in N-Triples and N-Quads, a delimiter must be on its own line
|
|
85
|
+
/** @param {{ line: number; type: string }} token */
|
|
71
86
|
_onToken(token) {
|
|
72
87
|
const line = token.line, delimiter = token.type === 'MESSAGE';
|
|
73
88
|
if (this._lineMode && !this._failed &&
|
|
@@ -78,6 +93,9 @@ export default class MessageParserCore {
|
|
|
78
93
|
this._line = line;
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
/** @param {Error | null} error
|
|
97
|
+
* @param {Q | null} quad
|
|
98
|
+
* @param {Record<string, import('@rdfjs/types').NamedNode>} [prefixes] */
|
|
81
99
|
_onQuad(error, quad, prefixes) {
|
|
82
100
|
if (this._failed || this._finished)
|
|
83
101
|
return;
|
|
@@ -93,6 +111,7 @@ export default class MessageParserCore {
|
|
|
93
111
|
}
|
|
94
112
|
}
|
|
95
113
|
|
|
114
|
+
/** @param {Q} quad */
|
|
96
115
|
_addQuad(quad) {
|
|
97
116
|
this._seenQuad = true;
|
|
98
117
|
// Blank node labels are scoped to their message
|
|
@@ -105,6 +124,7 @@ export default class MessageParserCore {
|
|
|
105
124
|
this._sink.quad(quad, this._index);
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
/** @param {string} label */
|
|
108
128
|
_announce(label) {
|
|
109
129
|
const error = this._checkVersion(label);
|
|
110
130
|
if (error)
|
|
@@ -119,6 +139,8 @@ export default class MessageParserCore {
|
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
// Validates a version label, and returns an error for unsupported ones
|
|
142
|
+
/** @param {string} label
|
|
143
|
+
* @param {boolean} [throwing] */
|
|
122
144
|
_checkVersion(label, throwing) {
|
|
123
145
|
if (isSupportedVersion(baseVersion(label), this._supportedVersions))
|
|
124
146
|
return null;
|
|
@@ -144,6 +166,7 @@ export default class MessageParserCore {
|
|
|
144
166
|
this._sink.message(quads, this._index++);
|
|
145
167
|
}
|
|
146
168
|
|
|
169
|
+
/** @param {Error} error */
|
|
147
170
|
_fail(error) {
|
|
148
171
|
this._failed = true;
|
|
149
172
|
this._current = null;
|
|
@@ -152,6 +175,8 @@ export default class MessageParserCore {
|
|
|
152
175
|
|
|
153
176
|
// ## Blank node scoping
|
|
154
177
|
|
|
178
|
+
/** @param {Q} quad
|
|
179
|
+
* @returns {Q} */
|
|
155
180
|
_scopeQuad(quad) {
|
|
156
181
|
const subject = this._scopeTerm(quad.subject);
|
|
157
182
|
const predicate = this._scopeTerm(quad.predicate);
|
|
@@ -162,6 +187,9 @@ export default class MessageParserCore {
|
|
|
162
187
|
quad : this._factory.quad(subject, predicate, object, graph);
|
|
163
188
|
}
|
|
164
189
|
|
|
190
|
+
/** @template {BaseTerm} T
|
|
191
|
+
* @param {T} term
|
|
192
|
+
* @returns {T} */
|
|
165
193
|
_scopeTerm(term) {
|
|
166
194
|
if (term.termType === 'BlankNode') {
|
|
167
195
|
const scope = this._scope || (this._scope = new Map());
|
|
@@ -171,8 +199,9 @@ export default class MessageParserCore {
|
|
|
171
199
|
this._factory.blankNode(`${this._blankNodePrefix}${this._blankNodeCounter++}`);
|
|
172
200
|
scope.set(term.value, scoped);
|
|
173
201
|
}
|
|
174
|
-
return scoped;
|
|
202
|
+
return /** @type {T} */ (/** @type {unknown} */ (scoped));
|
|
175
203
|
}
|
|
176
|
-
return term.termType === 'Quad' ?
|
|
204
|
+
return term.termType === 'Quad' ? /** @type {T} */ (/** @type {unknown} */ (
|
|
205
|
+
this._scopeQuad(/** @type {Q} */ (/** @type {unknown} */ (term))))) : term;
|
|
177
206
|
}
|
|
178
207
|
}
|
|
@@ -6,10 +6,14 @@ import { Transform } from 'stream';
|
|
|
6
6
|
import { StringDecoder } from 'string_decoder';
|
|
7
7
|
import MessageParserCore from './MessageParserCore.js';
|
|
8
8
|
|
|
9
|
+
/** @template {import('@rdfjs/types').BaseQuad} [Q=import('@rdfjs/types').Quad] */
|
|
9
10
|
export default class MessageStreamParser extends Transform {
|
|
11
|
+
/** @param {import('../common.js').MessageParserOptions<Q>} [options] */
|
|
10
12
|
constructor(options) {
|
|
11
13
|
super({ decodeStrings: false, readableObjectMode: true });
|
|
12
14
|
this._decoder = new StringDecoder('utf8');
|
|
15
|
+
|
|
16
|
+
/** @type {Error | null} */
|
|
13
17
|
this._error = null;
|
|
14
18
|
this._core = new MessageParserCore(options, {
|
|
15
19
|
message: quads => this.push(quads),
|
|
@@ -20,11 +24,15 @@ export default class MessageStreamParser extends Transform {
|
|
|
20
24
|
});
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
/** @param {string | Buffer} chunk
|
|
28
|
+
* @param {BufferEncoding} encoding
|
|
29
|
+
* @param {import('node:stream').TransformCallback} done */
|
|
23
30
|
_transform(chunk, encoding, done) {
|
|
24
31
|
this._core.write(typeof chunk === 'string' ? chunk : this._decoder.write(chunk));
|
|
25
32
|
done(this._error);
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
/** @param {import('node:stream').TransformCallback} done */
|
|
28
36
|
_flush(done) {
|
|
29
37
|
this._core.write(this._decoder.end());
|
|
30
38
|
this._core.end();
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
// **MessageStreamWriter** is a Node.js stream that serializes RDF Messages.
|
|
2
2
|
//
|
|
3
|
-
// It consumes messages (arrays of quads) and produces text.
|
|
3
|
+
// It consumes messages (arrays of quads), or `{ quad, messageIndex }` objects, and produces text.
|
|
4
4
|
import { Transform } from 'stream';
|
|
5
5
|
import MessageWriter from './MessageWriter.js';
|
|
6
6
|
|
|
7
7
|
export default class MessageStreamWriter extends Transform {
|
|
8
|
+
/** @param {import('../common.js').MessageWriterOptions} [options] */
|
|
8
9
|
constructor(options) {
|
|
9
10
|
super({ writableObjectMode: true, encoding: 'utf8' });
|
|
10
11
|
// Writing into this stream never blocks, and output is pushed synchronously
|
|
12
|
+
/** @type {Pick<import('./internal.js').Output, 'write'>} */
|
|
11
13
|
const output = { write: (chunk, encoding, done) => (this.push(chunk), done && done()) };
|
|
12
14
|
this._writer = new MessageWriter(output, Object.assign({}, options, { end: false }));
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/** @param {import('../browser.js').MessageWriterInput} message
|
|
18
|
+
* @param {BufferEncoding} encoding
|
|
19
|
+
* @param {import('node:stream').TransformCallback} done */
|
|
15
20
|
_transform(message, encoding, done) {
|
|
16
|
-
|
|
21
|
+
// A message (array of quads), or a quad of a message: `{ quad, messageIndex }`
|
|
22
|
+
if (Array.isArray(message)) this._writer.addMessage(message, done);
|
|
23
|
+
else this._writer.addQuad(message.quad, message.messageIndex, done);
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
/** @param {import('node:stream').TransformCallback} done */
|
|
19
27
|
_flush(done) {
|
|
20
28
|
this._writer.end(done);
|
|
21
29
|
}
|
package/src/MessageWriter.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { Writer } from 'n3';
|
|
10
10
|
import { resolveFormat, isMessagesVersion, baseVersion, isSupportedVersion } from './formats.js';
|
|
11
11
|
|
|
12
|
+
/** @extends {Writer<import('@rdfjs/types').BaseQuad>} */
|
|
12
13
|
class LogWriter extends Writer {
|
|
13
14
|
// ### `addDelimiter` ends the current message
|
|
14
15
|
addDelimiter() {
|
|
@@ -17,11 +18,22 @@ class LogWriter extends Writer {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** @typedef {import('./internal.js').Done} Done */
|
|
22
|
+
/** @typedef {import('./internal.js').End} End */
|
|
23
|
+
/** @typedef {import('../common.js').MessageWriterOptions} Options */
|
|
24
|
+
/** @typedef {Pick<import('./internal.js').Output, 'write'> & Partial<Pick<import('./internal.js').Output, 'end'>>} Output */
|
|
20
25
|
export default class MessageWriter {
|
|
21
26
|
// `outputStream` is optional: without it, `end` returns the output as a string
|
|
27
|
+
/** @overload
|
|
28
|
+
* @param {Options} [options] */
|
|
29
|
+
/** @overload
|
|
30
|
+
* @param {Output | null} outputStream
|
|
31
|
+
* @param {Options} [options] */
|
|
32
|
+
/** @param {Output | Options | null} [outputStream]
|
|
33
|
+
* @param {Options} [options] */
|
|
22
34
|
constructor(outputStream, options) {
|
|
23
|
-
if (outputStream && typeof outputStream.write !== 'function')
|
|
24
|
-
options = outputStream, outputStream = null;
|
|
35
|
+
if (outputStream && typeof /** @type {Output} */ (outputStream).write !== 'function')
|
|
36
|
+
options = /** @type {Options} */ (outputStream), outputStream = null;
|
|
25
37
|
options = options || {};
|
|
26
38
|
this._format = resolveFormat(options.format);
|
|
27
39
|
this._options = options;
|
|
@@ -30,45 +42,105 @@ export default class MessageWriter {
|
|
|
30
42
|
throw new TypeError(`Unsupported RDF Messages version: "${this._version}"`);
|
|
31
43
|
|
|
32
44
|
this._output = '';
|
|
33
|
-
this._stream = outputStream;
|
|
45
|
+
this._stream = /** @type {Output | null | undefined} */ (outputStream);
|
|
34
46
|
this._endStream = !outputStream || options.end !== false;
|
|
35
47
|
this._pending = 0;
|
|
48
|
+
|
|
49
|
+
/** @type {Done[]} */
|
|
36
50
|
this._waiting = [];
|
|
51
|
+
|
|
52
|
+
/** @type {Error | null} */
|
|
37
53
|
this._error = null;
|
|
38
54
|
this._started = false;
|
|
39
55
|
this._ended = false;
|
|
56
|
+
this._open = false;
|
|
57
|
+
this._count = 0; // messages begun so far: the index of the next message
|
|
40
58
|
this._declared = Object.assign({}, options.prefixes);
|
|
59
|
+
|
|
60
|
+
/** @type {Record<string, string>} */
|
|
41
61
|
this._newPrefixes = {};
|
|
42
62
|
// The N3.js writer writes to this sink, which it never ends
|
|
63
|
+
/** @type {Pick<Output, 'write'>} */
|
|
43
64
|
this._sink = { write: (chunk, encoding, done) => this._write(chunk, done) };
|
|
65
|
+
|
|
66
|
+
/** @type {LogWriter | null} */
|
|
44
67
|
this._writer = null;
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
// ### `addMessage` writes the quads of one message, followed by a delimiter.
|
|
48
71
|
// The callback (or returned promise) completes once the output is flushed.
|
|
72
|
+
/** @param {import('@rdfjs/types').BaseQuad[]} quads
|
|
73
|
+
* @param {Done} [done] */
|
|
49
74
|
addMessage(quads, done) {
|
|
50
75
|
return this._complete(done, () => {
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
76
|
+
if (this._open) throw new Error('A message is already open');
|
|
77
|
+
this._begin();
|
|
78
|
+
for (const quad of quads || []) this._add(quad);
|
|
79
|
+
this._close();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ### `beginMessage` opens a message. It is optional: the first `addQuad` opens one implicitly.
|
|
84
|
+
/** @param {Done} [done] */
|
|
85
|
+
beginMessage(done) {
|
|
86
|
+
return this._complete(done, () => {
|
|
87
|
+
if (this._open) throw new Error('A message is already open');
|
|
88
|
+
this._begin();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ### `addQuad` writes a quad into the open message right away (a message is opened if there is none).
|
|
93
|
+
// With a `messageIndex` (0-based), a change of index ends the current message and writes the empty
|
|
94
|
+
// messages in between, as rdf-writer-ts does; a lower index than the current one is an error.
|
|
95
|
+
/** @param {import('@rdfjs/types').BaseQuad} quad
|
|
96
|
+
* @param {number | Done} [messageIndex]
|
|
97
|
+
* @param {Done} [done] */
|
|
98
|
+
addQuad(quad, messageIndex, done) {
|
|
99
|
+
if (typeof messageIndex === 'function') done = messageIndex, messageIndex = undefined;
|
|
100
|
+
return this._complete(done, () => {
|
|
101
|
+
if (messageIndex !== undefined && messageIndex !== null) this._moveTo(messageIndex);
|
|
102
|
+
else if (!this._open) this._begin();
|
|
103
|
+
this._add(quad);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ### `endMessage` ends the open message with a delimiter. Without an open message, it writes an empty one.
|
|
108
|
+
/** @param {Done} [done] */
|
|
109
|
+
endMessage(done) {
|
|
110
|
+
return this._complete(done, () => {
|
|
111
|
+
if (!this._open) this._begin();
|
|
112
|
+
this._close();
|
|
53
113
|
});
|
|
54
114
|
}
|
|
55
115
|
|
|
56
116
|
// ### `addPrefix` declares a prefix, which is written before the next message
|
|
57
117
|
// and stays in effect for all messages after it
|
|
118
|
+
/** @param {string} prefix
|
|
119
|
+
* @param {string | import('@rdfjs/types').NamedNode} iri
|
|
120
|
+
* @param {Done} [done] */
|
|
58
121
|
addPrefix(prefix, iri, done) {
|
|
59
122
|
return this._complete(done, () => {
|
|
60
|
-
if (!this._format.lineMode && this._declared[prefix] !== (iri
|
|
61
|
-
this._newPrefixes[prefix] = iri
|
|
123
|
+
if (!this._format.lineMode && this._declared[prefix] !== (typeof iri === 'string' ? iri : iri.value))
|
|
124
|
+
this._newPrefixes[prefix] = typeof iri === 'string' ? iri : iri.value;
|
|
62
125
|
});
|
|
63
126
|
}
|
|
64
127
|
|
|
65
128
|
// ### `end` ends the log and, unless disabled, the output stream.
|
|
66
129
|
// The callback receives the output as a string if no output stream was given.
|
|
130
|
+
/** @overload
|
|
131
|
+
* @returns {Promise<string | void>} */
|
|
132
|
+
/** @overload
|
|
133
|
+
* @param {End} done
|
|
134
|
+
* @returns {void} */
|
|
135
|
+
/** @param {End} [done]
|
|
136
|
+
* @returns {Promise<string | void> | void} */
|
|
67
137
|
end(done) {
|
|
68
138
|
// Ending twice is harmless
|
|
69
139
|
if (this._ended)
|
|
70
140
|
return typeof done === 'function' ? done(null) : Promise.resolve();
|
|
71
141
|
this._start();
|
|
142
|
+
if (this._open)
|
|
143
|
+
this._close();
|
|
72
144
|
if (typeof done === 'function')
|
|
73
145
|
return this._finish(done);
|
|
74
146
|
return new Promise((resolve, reject) => this._finish((error, result) => error ? reject(error) : resolve(result)));
|
|
@@ -77,6 +149,9 @@ export default class MessageWriter {
|
|
|
77
149
|
// ## Private methods
|
|
78
150
|
|
|
79
151
|
// Runs an action and reports completion with a callback or a promise
|
|
152
|
+
/** @param {Done | undefined} done
|
|
153
|
+
* @param {() => void} action
|
|
154
|
+
* @returns {Promise<void> | void} */
|
|
80
155
|
_complete(done, action) {
|
|
81
156
|
if (this._ended)
|
|
82
157
|
this._error = this._error || new Error('Cannot write after end');
|
|
@@ -85,7 +160,7 @@ export default class MessageWriter {
|
|
|
85
160
|
action();
|
|
86
161
|
}
|
|
87
162
|
catch (error) {
|
|
88
|
-
this._error = this._error || error;
|
|
163
|
+
this._error = this._error || /** @type {Error} */ (error);
|
|
89
164
|
}
|
|
90
165
|
if (typeof done === 'function')
|
|
91
166
|
return this._whenFlushed(done);
|
|
@@ -93,25 +168,29 @@ export default class MessageWriter {
|
|
|
93
168
|
}
|
|
94
169
|
|
|
95
170
|
// Ends the output once all queued writes were accepted
|
|
171
|
+
/** @param {End} callback */
|
|
96
172
|
_finish(callback) {
|
|
97
173
|
this._ended = true;
|
|
98
174
|
this._whenFlushed(error => this._endOutput(error, callback));
|
|
99
175
|
}
|
|
100
176
|
|
|
177
|
+
/** @param {Error | null | undefined} error
|
|
178
|
+
* @param {End} callback */
|
|
101
179
|
_endOutput(error, callback) {
|
|
102
180
|
if (error || !this._endStream)
|
|
103
181
|
return callback(error);
|
|
104
182
|
if (!this._stream)
|
|
105
183
|
return callback(null, this._output);
|
|
106
184
|
try {
|
|
107
|
-
this._stream.end(endError => callback(endError));
|
|
185
|
+
(/** @type {Required<Output>} */ (this._stream)).end(endError => callback(endError));
|
|
108
186
|
}
|
|
109
187
|
catch (endError) {
|
|
110
|
-
callback(endError);
|
|
188
|
+
callback(/** @type {Error} */ (endError));
|
|
111
189
|
}
|
|
112
190
|
}
|
|
113
191
|
|
|
114
192
|
// Calls back once all queued writes were accepted by the output
|
|
193
|
+
/** @param {Done} done */
|
|
115
194
|
_whenFlushed(done) {
|
|
116
195
|
if (this._pending === 0)
|
|
117
196
|
this._settle(done);
|
|
@@ -119,12 +198,15 @@ export default class MessageWriter {
|
|
|
119
198
|
this._waiting.push(done);
|
|
120
199
|
}
|
|
121
200
|
|
|
201
|
+
/** @param {Done} done */
|
|
122
202
|
_settle(done) {
|
|
123
203
|
const error = this._error;
|
|
124
204
|
this._error = null;
|
|
125
205
|
done(error || null);
|
|
126
206
|
}
|
|
127
207
|
|
|
208
|
+
/** @param {string} chunk
|
|
209
|
+
* @param {Done} [done] */
|
|
128
210
|
_write(chunk, done) {
|
|
129
211
|
if (!this._stream) {
|
|
130
212
|
this._output += chunk;
|
|
@@ -135,11 +217,13 @@ export default class MessageWriter {
|
|
|
135
217
|
this._stream.write(chunk, 'utf8', error => this._written(error, done));
|
|
136
218
|
}
|
|
137
219
|
catch (error) {
|
|
138
|
-
this._written(error, done);
|
|
220
|
+
this._written(/** @type {Error} */ (error), done);
|
|
139
221
|
}
|
|
140
222
|
}
|
|
141
223
|
|
|
142
224
|
// Registers that the output accepted a write, and wakes up those waiting for the output
|
|
225
|
+
/** @param {Error | null | undefined} error
|
|
226
|
+
* @param {Done} [done] */
|
|
143
227
|
_written(error, done) {
|
|
144
228
|
this._pending--;
|
|
145
229
|
this._error = this._error || error || null;
|
|
@@ -165,32 +249,66 @@ export default class MessageWriter {
|
|
|
165
249
|
}
|
|
166
250
|
}
|
|
167
251
|
|
|
168
|
-
|
|
169
|
-
|
|
252
|
+
// Opens a message: declares the prefixes that were added since the previous one
|
|
253
|
+
_begin() {
|
|
254
|
+
this._start();
|
|
255
|
+
const writer = /** @type {LogWriter} */ (this._writer);
|
|
170
256
|
for (const prefix of Object.keys(this._newPrefixes)) {
|
|
171
|
-
|
|
172
|
-
|
|
257
|
+
const iri = /** @type {string} */ (this._newPrefixes[prefix]);
|
|
258
|
+
writer.addPrefix(prefix, iri);
|
|
259
|
+
this._declared[prefix] = iri;
|
|
173
260
|
}
|
|
174
261
|
this._newPrefixes = {};
|
|
262
|
+
this._open = true;
|
|
263
|
+
this._count++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @param {import('@rdfjs/types').BaseQuad} quad */
|
|
267
|
+
_add(quad) {
|
|
268
|
+
/** @type {Done} */
|
|
175
269
|
const record = error => error && (this._error = this._error || error);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
270
|
+
(/** @type {LogWriter} */ (this._writer)).addQuad(quad.subject, quad.predicate, quad.object, quad.graph, record);
|
|
271
|
+
}
|
|
272
|
+
_close() {
|
|
273
|
+
this._open = false;
|
|
274
|
+
(/** @type {LogWriter} */ (this._writer)).addDelimiter();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Makes message `index` the open message, writing empty messages for the gap
|
|
278
|
+
/** @param {number} index */
|
|
279
|
+
_moveTo(index) {
|
|
280
|
+
const current = this._count - 1;
|
|
281
|
+
if (!Number.isInteger(index) || index < 0) throw new RangeError(`Invalid message index: ${index}`);
|
|
282
|
+
if (this._open && index === current) return;
|
|
283
|
+
if (index <= current) throw new RangeError(`Message index ${index} is lower than the current message index ${current}`);
|
|
284
|
+
if (this._open) this._close();
|
|
285
|
+
while (this._count < index) {
|
|
286
|
+
this._begin();
|
|
287
|
+
this._close();
|
|
288
|
+
}
|
|
289
|
+
this._begin();
|
|
179
290
|
}
|
|
180
291
|
}
|
|
181
292
|
|
|
293
|
+
/** @param {Iterable<import('@rdfjs/types').BaseQuad[]>} messages
|
|
294
|
+
* @param {Options} [options]
|
|
295
|
+
* @returns {string} */
|
|
182
296
|
// ### `writeMessages` synchronously serializes an array of messages to a string
|
|
183
297
|
export function writeMessages(messages, options) {
|
|
184
298
|
const writer = new MessageWriter(options);
|
|
299
|
+
|
|
300
|
+
/** @type {Error[]} */
|
|
185
301
|
const errors = [];
|
|
186
|
-
let output;
|
|
302
|
+
let output = '';
|
|
303
|
+
|
|
304
|
+
/** @param {Error | null | undefined} error */
|
|
187
305
|
function collect(error) {
|
|
188
306
|
if (error)
|
|
189
307
|
errors.push(error);
|
|
190
308
|
}
|
|
191
309
|
for (const message of messages)
|
|
192
310
|
writer.addMessage(message, collect);
|
|
193
|
-
writer.end((error, result) => (collect(error), output = result));
|
|
311
|
+
writer.end((error, result) => (collect(error), output = /** @type {string} */ (result)));
|
|
194
312
|
if (errors.length > 0)
|
|
195
313
|
throw errors[0];
|
|
196
314
|
return output;
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import MessageParserCore from './MessageParserCore.js';
|
|
2
|
+
import MessageWriter from './MessageWriter.js';
|
|
3
|
+
|
|
4
|
+
export { default as MessageParser, parseMessages } from './MessageParser.js';
|
|
5
|
+
export { default as MessageWriter, writeMessages } from './MessageWriter.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A Web Streams parser; strings and UTF-8 bytes can be piped directly from fetch.
|
|
9
|
+
* @template {import('@rdfjs/types').BaseQuad} [Q=import('@rdfjs/types').Quad]
|
|
10
|
+
* @extends {TransformStream<import('../browser.js').MessageChunk, Q[]>}
|
|
11
|
+
*/
|
|
12
|
+
export class MessageStreamParser extends TransformStream {
|
|
13
|
+
/** @param {import('../browser.js').BrowserParserOptions<Q>} [options] */
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
const decoder = new TextDecoder();
|
|
16
|
+
|
|
17
|
+
/** @type {MessageParserCore<Q>} */
|
|
18
|
+
let core;
|
|
19
|
+
super({
|
|
20
|
+
start(controller) {
|
|
21
|
+
core = new MessageParserCore(options, {
|
|
22
|
+
message: quads => controller.enqueue(quads),
|
|
23
|
+
end: () => {},
|
|
24
|
+
error: error => { throw error; },
|
|
25
|
+
prefix: options.onPrefix,
|
|
26
|
+
version: options.onVersion,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
transform(chunk) {
|
|
30
|
+
core.write(typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }));
|
|
31
|
+
},
|
|
32
|
+
flush() {
|
|
33
|
+
core.write(decoder.decode());
|
|
34
|
+
core.end();
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A Web Streams writer; accepts messages or individual indexed quads.
|
|
41
|
+
* @extends {TransformStream<import('../browser.js').MessageWriterInput, string>} */
|
|
42
|
+
export class MessageStreamWriter extends TransformStream {
|
|
43
|
+
/** @param {import('../common.js').MessageWriterOptions} [options] */
|
|
44
|
+
constructor(options) {
|
|
45
|
+
/** @type {MessageWriter} */
|
|
46
|
+
let writer;
|
|
47
|
+
super({
|
|
48
|
+
start(controller) {
|
|
49
|
+
/** @type {Pick<import('./internal.js').Output, 'write'>} */
|
|
50
|
+
const output = { write: (chunk, encoding, done) => { controller.enqueue(chunk); done(); } };
|
|
51
|
+
writer = new MessageWriter(output, { ...options, end: false });
|
|
52
|
+
},
|
|
53
|
+
transform(message) {
|
|
54
|
+
return Array.isArray(message) ? writer.addMessage(message) : writer.addQuad(message.quad, message.messageIndex);
|
|
55
|
+
},
|
|
56
|
+
flush() {
|
|
57
|
+
return writer.end().then(() => {});
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/formats.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// **formats** describes the RDF syntaxes that support RDF Messages.
|
|
2
2
|
|
|
3
|
+
/** @type {Record<string, { name: string; lineMode: boolean }>} */
|
|
3
4
|
const FORMATS = {
|
|
4
5
|
turtle: { name: 'Turtle', lineMode: false },
|
|
5
6
|
trig: { name: 'TriG', lineMode: false },
|
|
@@ -12,7 +13,7 @@ const BASE_VERSIONS = ['1.2', '1.2-basic', '1.1'];
|
|
|
12
13
|
|
|
13
14
|
// ### `resolveFormat` maps a format name or media type to a supported syntax
|
|
14
15
|
export function resolveFormat(format = 'TriG') {
|
|
15
|
-
const key = String(format).toLowerCase().
|
|
16
|
+
const key = String(format).toLowerCase().replace(/;[\s\S]*$/, '').trim()
|
|
16
17
|
.replace(/^(?:text|application)\//, '')
|
|
17
18
|
.replace(/^x-/, '')
|
|
18
19
|
.replace(/[-_ ]/g, '');
|
|
@@ -23,16 +24,19 @@ export function resolveFormat(format = 'TriG') {
|
|
|
23
24
|
return resolved;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/** @param {unknown} version */
|
|
26
28
|
// ### `isMessagesVersion` checks whether a version label announces RDF Messages
|
|
27
29
|
export function isMessagesVersion(version) {
|
|
28
30
|
return typeof version === 'string' && version.endsWith('-messages');
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** @param {string} version */
|
|
31
34
|
// ### `baseVersion` strips the `-messages` suffix of a version label
|
|
32
35
|
export function baseVersion(version) {
|
|
33
36
|
return isMessagesVersion(version) ? version.slice(0, -'-messages'.length) : version;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/** @param {string} version */
|
|
36
40
|
// ### `isSupportedVersion` checks whether a (base) version label is supported
|
|
37
41
|
export function isSupportedVersion(version, supported = BASE_VERSIONS) {
|
|
38
42
|
return supported.includes(version);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as RDF from '@rdfjs/types';
|
|
2
|
+
export interface ParserSink<Q extends RDF.BaseQuad> {
|
|
3
|
+
message(quads: Q[], index: number): unknown;
|
|
4
|
+
end(prefixes?: Record<string, RDF.NamedNode>): unknown;
|
|
5
|
+
error(error: Error): unknown;
|
|
6
|
+
quad?(quad: Q, index: number): unknown;
|
|
7
|
+
prefix?(prefix: string, term: RDF.NamedNode): unknown;
|
|
8
|
+
version?(label: string): unknown;
|
|
9
|
+
}
|
|
10
|
+
export type Done = (error?: Error | null) => void;
|
|
11
|
+
export type End = (error?: Error | null, result?: string) => void;
|
|
12
|
+
export interface Output {
|
|
13
|
+
write(chunk: string, encoding: string, done: Done): unknown;
|
|
14
|
+
end(done: Done): unknown;
|
|
15
|
+
}
|