eyeling 1.28.0 → 1.28.2
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 +6 -13
- package/dist/browser/eyeling.browser.js +252 -28
- package/examples/alma-rdf-messages.n3 +202 -0
- package/examples/output/alma-rdf-messages.n3 +0 -0
- package/eyeling.js +259 -29
- package/lib/cli.js +140 -27
- package/lib/engine.js +9 -1
- package/lib/lexer.js +103 -0
- package/notes/rdf-message-logs.md +228 -0
- package/package.json +1 -1
- package/test/api.test.js +16 -0
- package/test/stream_messages.test.js +102 -3
- package/tools/bundle.js +7 -1
package/lib/lexer.js
CHANGED
|
@@ -515,11 +515,114 @@ function normalizeRdfCompatibility(inputText) {
|
|
|
515
515
|
return text.slice(at, j);
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
function readBalancedTermAt(text, at) {
|
|
519
|
+
const open = text[at];
|
|
520
|
+
const matching = { '{': '}', '[': ']', '(': ')' };
|
|
521
|
+
if (!matching[open]) return null;
|
|
522
|
+
|
|
523
|
+
const stack = [matching[open]];
|
|
524
|
+
let j = at + 1;
|
|
525
|
+
while (j < text.length) {
|
|
526
|
+
const ch = text[j];
|
|
527
|
+
if (ch === '"' || ch === "'") {
|
|
528
|
+
const str = readStringAt(text, j);
|
|
529
|
+
j = str.end;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (ch === '<' && !text.startsWith('<<', j)) {
|
|
533
|
+
const iri = readIriAt(text, j);
|
|
534
|
+
j = iri.end;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (ch === '#') {
|
|
538
|
+
while (j < text.length && text[j] !== '\n' && text[j] !== '\r') j += 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (matching[ch]) {
|
|
542
|
+
stack.push(matching[ch]);
|
|
543
|
+
j += 1;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (ch === stack[stack.length - 1]) {
|
|
547
|
+
stack.pop();
|
|
548
|
+
j += 1;
|
|
549
|
+
if (stack.length === 0) return { text: text.slice(at, j), end: j };
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
j += 1;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
throw new N3SyntaxError(`Unterminated term inside RDF 1.2 triple term, expected ${stack[stack.length - 1]}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function readRdfTripleTermComponent(text, at) {
|
|
559
|
+
const j = skipWsAndComments(text, at);
|
|
560
|
+
if (j >= text.length) return null;
|
|
561
|
+
const ch = text[j];
|
|
562
|
+
|
|
563
|
+
if (ch === '<') return readIriAt(text, j);
|
|
564
|
+
|
|
565
|
+
if (ch === '"' || ch === "'") {
|
|
566
|
+
const str = readStringAt(text, j);
|
|
567
|
+
let end = str.end;
|
|
568
|
+
let termText = str.text;
|
|
569
|
+
if (text.startsWith('^^', end)) {
|
|
570
|
+
const datatype = readRdfTripleTermComponent(text, end + 2);
|
|
571
|
+
if (datatype) {
|
|
572
|
+
termText += '^^' + datatype.text;
|
|
573
|
+
end = datatype.end;
|
|
574
|
+
}
|
|
575
|
+
} else if (text[end] === '@') {
|
|
576
|
+
let k = end + 1;
|
|
577
|
+
if (/[A-Za-z]/.test(text[k] || '')) {
|
|
578
|
+
while (k < text.length && /[A-Za-z0-9-]/.test(text[k])) k += 1;
|
|
579
|
+
termText += text.slice(end, k);
|
|
580
|
+
end = k;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { text: termText, end };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (ch === '{' || ch === '[' || ch === '(') return readBalancedTermAt(text, j);
|
|
587
|
+
|
|
588
|
+
let k = j;
|
|
589
|
+
while (k < text.length && !/\s/.test(text[k]) && !'{}[](),;'.includes(text[k])) k += 1;
|
|
590
|
+
if (k === j) return null;
|
|
591
|
+
const value = text.slice(j, k);
|
|
592
|
+
if (!value || value.startsWith('@')) return null;
|
|
593
|
+
return { text: value, end: k };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function validateSingleRdfTripleTerm(rawTriple) {
|
|
597
|
+
const triple = rawTriple.trim();
|
|
598
|
+
if (!triple) throw new N3SyntaxError('RDF 1.2 triple term must contain exactly one subject, predicate, and object');
|
|
599
|
+
|
|
600
|
+
let pos = 0;
|
|
601
|
+
for (const label of ['subject', 'predicate', 'object']) {
|
|
602
|
+
const term = readRdfTripleTermComponent(triple, pos);
|
|
603
|
+
if (!term) throw new N3SyntaxError(`RDF 1.2 triple term is missing a ${label}`);
|
|
604
|
+
pos = term.end;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const rest = skipWsAndComments(triple, pos);
|
|
608
|
+
if (rest >= triple.length) return;
|
|
609
|
+
|
|
610
|
+
const found = triple[rest];
|
|
611
|
+
if (found === ',') {
|
|
612
|
+
throw new N3SyntaxError("RDF 1.2 triple terms must contain exactly one object; object lists using ',' are not valid inside <<( ... )>>");
|
|
613
|
+
}
|
|
614
|
+
if (found === ';') {
|
|
615
|
+
throw new N3SyntaxError("RDF 1.2 triple terms must contain exactly one predicate-object pair; ';' is not valid inside <<( ... )>>");
|
|
616
|
+
}
|
|
617
|
+
throw new N3SyntaxError(`RDF 1.2 triple term must contain exactly one subject, predicate, and object; unexpected ${JSON.stringify(triple.slice(rest, rest + 20))}`);
|
|
618
|
+
}
|
|
619
|
+
|
|
518
620
|
function graphTermFromTripleBody(rawBody, parenthesized) {
|
|
519
621
|
let body = rawBody.trim();
|
|
520
622
|
if (parenthesized && body.startsWith('(') && body.endsWith(')')) body = body.slice(1, -1).trim();
|
|
521
623
|
const split = splitTopLevelReifier(body);
|
|
522
624
|
const triple = split.triple;
|
|
625
|
+
validateSingleRdfTripleTerm(triple);
|
|
523
626
|
const graph = '{ ' + triple + ' }';
|
|
524
627
|
if (split.reifier) {
|
|
525
628
|
const reifier = firstTerm(split.reifier);
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# RDF Message Logs internals
|
|
2
|
+
|
|
3
|
+
Eyeling handles RDF Message Logs as an RDF-compatibility normalization step. A message log is not parsed as one flat graph first; it is split into message chunks and rewritten into ordinary N3 facts that describe the stream, its ordered message envelopes, and each message payload graph.
|
|
4
|
+
|
|
5
|
+
There are two execution paths:
|
|
6
|
+
|
|
7
|
+
- normal RDF mode, where a whole message log is normalized into one replay document;
|
|
8
|
+
- `--stream-messages`, where the CLI reads one message at a time and runs the rules once per replayed message.
|
|
9
|
+
|
|
10
|
+
Both paths expose the same basic `eymsg:` vocabulary and use N3 quoted formulas for payload graphs.
|
|
11
|
+
|
|
12
|
+
## Input shape
|
|
13
|
+
|
|
14
|
+
A log is recognized by a message-version directive:
|
|
15
|
+
|
|
16
|
+
```trig
|
|
17
|
+
VERSION "1.2-messages"
|
|
18
|
+
PREFIX : <urn:example#>
|
|
19
|
+
|
|
20
|
+
:a :value 1 .
|
|
21
|
+
|
|
22
|
+
MESSAGE
|
|
23
|
+
|
|
24
|
+
# Empty heartbeat.
|
|
25
|
+
|
|
26
|
+
MESSAGE
|
|
27
|
+
|
|
28
|
+
:b :value 2 .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The current code accepts message versions matching:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
VERSION "1.1-messages"
|
|
35
|
+
VERSION "1.2-messages"
|
|
36
|
+
VERSION "1.2-basic-messages"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Old-style `@version` and `@message` forms are also recognized.
|
|
40
|
+
|
|
41
|
+
## Normal, whole-log RDF mode
|
|
42
|
+
|
|
43
|
+
When `lex(input, { rdf: true })` sees a `*-messages` version directive, `normalizeRdfCompatibility()` dispatches to `normalizeRdfMessageLog()` in `lib/lexer.js`.
|
|
44
|
+
|
|
45
|
+
That function:
|
|
46
|
+
|
|
47
|
+
1. strips the version directive;
|
|
48
|
+
2. splits the text at top-level `MESSAGE` / `@message` delimiters;
|
|
49
|
+
3. creates deterministic stream, envelope, and payload IRIs from a hash of the whole source text;
|
|
50
|
+
4. normalizes each message chunk through the RDF/TriG compatibility layer;
|
|
51
|
+
5. rewrites blank-node labels so each message gets its own blank-node scope;
|
|
52
|
+
6. emits ordinary N3 replay facts.
|
|
53
|
+
|
|
54
|
+
Conceptually, the input above becomes something like:
|
|
55
|
+
|
|
56
|
+
```n3
|
|
57
|
+
<urn:eyeling:message-log:HASH#stream>
|
|
58
|
+
a eymsg:RDFMessageStream ;
|
|
59
|
+
eymsg:messageCount "3"^^xsd:integer ;
|
|
60
|
+
eymsg:orderedEnvelopes (
|
|
61
|
+
<urn:eyeling:message-log:HASH#m001>
|
|
62
|
+
<urn:eyeling:message-log:HASH#m002>
|
|
63
|
+
<urn:eyeling:message-log:HASH#m003>
|
|
64
|
+
) ;
|
|
65
|
+
eymsg:firstEnvelope <urn:eyeling:message-log:HASH#m001> ;
|
|
66
|
+
eymsg:lastEnvelope <urn:eyeling:message-log:HASH#m003> ;
|
|
67
|
+
eymsg:envelope <urn:eyeling:message-log:HASH#m001>,
|
|
68
|
+
<urn:eyeling:message-log:HASH#m002>,
|
|
69
|
+
<urn:eyeling:message-log:HASH#m003> .
|
|
70
|
+
|
|
71
|
+
<urn:eyeling:message-log:HASH#m001>
|
|
72
|
+
a eymsg:MessageEnvelope ;
|
|
73
|
+
eymsg:offset "1"^^xsd:integer ;
|
|
74
|
+
eymsg:payloadKind eymsg:nonEmpty ;
|
|
75
|
+
eymsg:nextEnvelope <urn:eyeling:message-log:HASH#m002> ;
|
|
76
|
+
eymsg:payloadGraph <urn:eyeling:message-log:HASH#m001/payload> .
|
|
77
|
+
|
|
78
|
+
<urn:eyeling:message-log:HASH#m001/payload>
|
|
79
|
+
log:nameOf {
|
|
80
|
+
:a :value 1 .
|
|
81
|
+
} .
|
|
82
|
+
|
|
83
|
+
<urn:eyeling:message-log:HASH#m002>
|
|
84
|
+
a eymsg:MessageEnvelope ;
|
|
85
|
+
eymsg:offset "2"^^xsd:integer ;
|
|
86
|
+
eymsg:payloadKind eymsg:empty ;
|
|
87
|
+
eymsg:nextEnvelope <urn:eyeling:message-log:HASH#m003> .
|
|
88
|
+
|
|
89
|
+
<urn:eyeling:message-log:HASH#m003>
|
|
90
|
+
a eymsg:MessageEnvelope ;
|
|
91
|
+
eymsg:offset "3"^^xsd:integer ;
|
|
92
|
+
eymsg:payloadKind eymsg:nonEmpty ;
|
|
93
|
+
eymsg:payloadGraph <urn:eyeling:message-log:HASH#m003/payload> .
|
|
94
|
+
|
|
95
|
+
<urn:eyeling:message-log:HASH#m003/payload>
|
|
96
|
+
log:nameOf {
|
|
97
|
+
:b :value 2 .
|
|
98
|
+
} .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
After this rewrite, the ordinary N3 parser and reasoner handle the result. There is no separate RDF Message Log AST.
|
|
102
|
+
|
|
103
|
+
## Payload graphs
|
|
104
|
+
|
|
105
|
+
Payloads are represented exactly like other RDF/TriG named graphs in RDF compatibility mode:
|
|
106
|
+
|
|
107
|
+
```n3
|
|
108
|
+
?Payload log:nameOf { ...payload triples... } .
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The object is an N3 quoted formula. Rules inspect it with formula-aware built-ins such as `log:includes`:
|
|
112
|
+
|
|
113
|
+
```n3
|
|
114
|
+
@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#> .
|
|
115
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
?Envelope eymsg:payloadGraph ?Payload .
|
|
119
|
+
?Payload log:nameOf ?Graph .
|
|
120
|
+
?Graph log:includes { ?Subject :value ?Value . } .
|
|
121
|
+
} => {
|
|
122
|
+
?Envelope :sawValue ?Value .
|
|
123
|
+
} .
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This is the key design choice: message boundaries are preserved by putting each payload in its own quoted graph instead of merging all message triples into the top-level fact set.
|
|
127
|
+
|
|
128
|
+
## Per-message normalization
|
|
129
|
+
|
|
130
|
+
Each message body is normalized before it is embedded as a payload formula:
|
|
131
|
+
|
|
132
|
+
- RDF 1.2 triple terms are converted to singleton N3 graph terms;
|
|
133
|
+
- RDF 1.2 annotation syntax is expanded;
|
|
134
|
+
- TriG named graphs are converted to `log:nameOf` triples;
|
|
135
|
+
- blank-node labels are rewritten with a message-specific prefix.
|
|
136
|
+
|
|
137
|
+
For example, the same blank-node label in two different messages does not denote the same internal blank node. Message 1 rewrites roughly to:
|
|
138
|
+
|
|
139
|
+
```n3
|
|
140
|
+
_:eyeling_m001_b :value 1 .
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
and message 3 rewrites roughly to:
|
|
144
|
+
|
|
145
|
+
```n3
|
|
146
|
+
_:eyeling_m003_b :value 2 .
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If the message log appears as the second parsed source, the parser's usual source prefixing may add another source prefix around those labels, for example `_:src2_eyeling_m001_b`.
|
|
150
|
+
|
|
151
|
+
## Empty heartbeat messages
|
|
152
|
+
|
|
153
|
+
A message chunk is considered empty when, after directives and comments are ignored, it contains no RDF payload.
|
|
154
|
+
|
|
155
|
+
Empty messages still get an envelope:
|
|
156
|
+
|
|
157
|
+
```n3
|
|
158
|
+
?Envelope a eymsg:MessageEnvelope ;
|
|
159
|
+
eymsg:offset "2"^^xsd:integer ;
|
|
160
|
+
eymsg:payloadKind eymsg:empty .
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
but they do not get an `eymsg:payloadGraph` triple. This lets rules distinguish heartbeats from non-empty data messages without accidentally reusing a previous payload.
|
|
164
|
+
|
|
165
|
+
## `--stream-messages` mode
|
|
166
|
+
|
|
167
|
+
The CLI streaming path lives in `lib/cli.js`.
|
|
168
|
+
|
|
169
|
+
With:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
eyeling --rdf --stream-messages rules.n3 messages.trig
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
the CLI separates ordinary sources from message-log sources. Ordinary sources are parsed once as the reusable rule/program documents. Message logs are then read chunk by chunk.
|
|
176
|
+
|
|
177
|
+
For each chunk, `buildSingleMessageReplayDocument()` creates a small replay document containing one stream, one envelope, and optionally one payload graph:
|
|
178
|
+
|
|
179
|
+
```n3
|
|
180
|
+
<urn:eyeling:message-stream:HASH#stream>
|
|
181
|
+
a eymsg:RDFMessageStream ;
|
|
182
|
+
eymsg:envelope <urn:eyeling:message-stream:HASH#m000001> ;
|
|
183
|
+
eymsg:orderedEnvelopes (<urn:eyeling:message-stream:HASH#m000001>) ;
|
|
184
|
+
eymsg:firstEnvelope <urn:eyeling:message-stream:HASH#m000001> ;
|
|
185
|
+
eymsg:lastEnvelope <urn:eyeling:message-stream:HASH#m000001> .
|
|
186
|
+
|
|
187
|
+
<urn:eyeling:message-stream:HASH#m000001>
|
|
188
|
+
a eymsg:MessageEnvelope ;
|
|
189
|
+
eymsg:offset "1"^^xsd:integer ;
|
|
190
|
+
eymsg:payloadKind eymsg:nonEmpty ;
|
|
191
|
+
eymsg:payloadGraph <urn:eyeling:message-stream:HASH#m000001/payload> .
|
|
192
|
+
|
|
193
|
+
<urn:eyeling:message-stream:HASH#m000001/payload>
|
|
194
|
+
log:nameOf {
|
|
195
|
+
...one message payload...
|
|
196
|
+
} .
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
That one-message replay document is merged with the already-parsed program sources and run immediately. The next message gets a fresh replay document and a fresh run.
|
|
200
|
+
|
|
201
|
+
The streaming replay document intentionally does not expose the whole global message list. It only exposes the current message envelope, because the point of `--stream-messages` is one-message-at-a-time processing without materializing the whole log.
|
|
202
|
+
|
|
203
|
+
## Remote logs
|
|
204
|
+
|
|
205
|
+
For local files, `--stream-messages` reads line by line. For HTTP(S) sources, the CLI first checks the prefix to detect a message-version directive. When processing a remote text/plain RDF Message Log, it downloads the source into a temporary file and then streams that local copy line by line.
|
|
206
|
+
|
|
207
|
+
## Output behavior
|
|
208
|
+
|
|
209
|
+
RDF Message Logs do not introduce special output syntax. Once replay facts have been produced, output is the normal Eyeling output path:
|
|
210
|
+
|
|
211
|
+
- `log:outputString` facts are collected and printed as text;
|
|
212
|
+
- `log:query` output is printed from query conclusions;
|
|
213
|
+
- in RDF mode, triples are printed with RDF-compatible output formatting where possible.
|
|
214
|
+
|
|
215
|
+
## Practical mental model
|
|
216
|
+
|
|
217
|
+
RDF Message Log support is best understood as:
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
message-log syntax
|
|
221
|
+
-> split into message chunks
|
|
222
|
+
-> normalize each chunk as RDF/TriG/RDF 1.2
|
|
223
|
+
-> wrap each chunk in an eymsg: envelope
|
|
224
|
+
-> expose the payload as log:nameOf { ... }
|
|
225
|
+
-> run the ordinary N3 reasoner
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
So the feature is not a separate streaming RDF reasoner. It is a replay encoding that turns message boundaries into explicit N3 facts and quoted payload graphs, which ordinary Eyeling rules can inspect.
|
package/package.json
CHANGED
package/test/api.test.js
CHANGED
|
@@ -2916,6 +2916,22 @@ _:b a ex:Person ; ex:name "B" .
|
|
|
2916
2916
|
expect: [/:observation\s+:entails\s+<<\(\s+:sensor\s+:needs\s+:inspection\s*\)>>\s*\./m],
|
|
2917
2917
|
},
|
|
2918
2918
|
|
|
2919
|
+
{
|
|
2920
|
+
name: 'RDF mode rejects object lists inside RDF 1.2 triple terms',
|
|
2921
|
+
opt: { proofComments: false, rdf: true },
|
|
2922
|
+
input: `VERSION "1.2"
|
|
2923
|
+
@prefix : <http://example.org/ns#> .
|
|
2924
|
+
@prefix coll: <http://example.org/collection#> .
|
|
2925
|
+
|
|
2926
|
+
:climateDataset coll:contains
|
|
2927
|
+
<<( :asiaDataset coll:contains
|
|
2928
|
+
<<( :China :avgTemp :18C )>>,
|
|
2929
|
+
<<( :Thailand :avgTemp :20C )>>
|
|
2930
|
+
)>> .
|
|
2931
|
+
`,
|
|
2932
|
+
expectError: true,
|
|
2933
|
+
},
|
|
2934
|
+
|
|
2919
2935
|
{
|
|
2920
2936
|
name: 'RDF mode accepts PREFIX and reified triple terms with reifiers',
|
|
2921
2937
|
opt: { proofComments: false, rdf: true },
|
|
@@ -161,6 +161,88 @@ function startFileServer(file) {
|
|
|
161
161
|
throw new Error('test HTTP server did not start');
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
|
|
165
|
+
function startOpenEndedMessageServer() {
|
|
166
|
+
const dir = mkTmpDir();
|
|
167
|
+
const script = path.join(dir, 'server.js');
|
|
168
|
+
const portFile = path.join(dir, 'server.port');
|
|
169
|
+
fs.writeFileSync(
|
|
170
|
+
script,
|
|
171
|
+
[
|
|
172
|
+
"const http = require('node:http');",
|
|
173
|
+
"const fs = require('node:fs');",
|
|
174
|
+
'const portFile = process.argv[2];',
|
|
175
|
+
'const server = http.createServer((req, res) => {',
|
|
176
|
+
" res.writeHead(200, { 'content-type': 'text/plain' });",
|
|
177
|
+
" res.write('VERSION \\\"1.2-messages\\\"\\nPREFIX : <urn:test#>\\n');",
|
|
178
|
+
" res.write(':a :line \\\"one\\\\n\\\".\\nMESSAGE\\n');",
|
|
179
|
+
" setInterval(() => res.write('# keepalive\\n'), 1000);",
|
|
180
|
+
'});',
|
|
181
|
+
"server.listen(0, '127.0.0.1', () => fs.writeFileSync(portFile, String(server.address().port)));",
|
|
182
|
+
'',
|
|
183
|
+
].join('\n'),
|
|
184
|
+
'utf8',
|
|
185
|
+
);
|
|
186
|
+
const child = cp.spawn(process.execPath, [script, portFile], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
187
|
+
const deadline = Date.now() + 5000;
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
if (fs.existsSync(portFile)) {
|
|
190
|
+
const port = fs.readFileSync(portFile, 'utf8').trim();
|
|
191
|
+
return {
|
|
192
|
+
url: `http://127.0.0.1:${port}/messages.txt`,
|
|
193
|
+
stop: () => {
|
|
194
|
+
child.kill();
|
|
195
|
+
rmrf(dir);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
200
|
+
}
|
|
201
|
+
child.kill();
|
|
202
|
+
rmrf(dir);
|
|
203
|
+
throw new Error('test open-ended HTTP server did not start');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function waitForEyelingOutput(args, pattern, opts = {}) {
|
|
207
|
+
const timeoutMs = opts.timeoutMs || 4000;
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const child = cp.spawn(process.execPath, [eyelingJsPath, ...args], {
|
|
210
|
+
cwd: root,
|
|
211
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
212
|
+
});
|
|
213
|
+
let stdout = '';
|
|
214
|
+
let stderr = '';
|
|
215
|
+
let settled = false;
|
|
216
|
+
const timer = setTimeout(() => {
|
|
217
|
+
if (settled) return;
|
|
218
|
+
settled = true;
|
|
219
|
+
child.kill();
|
|
220
|
+
reject(new Error(`timed out waiting for ${pattern}; stdout=${JSON.stringify(stdout)} stderr=${JSON.stringify(stderr)}`));
|
|
221
|
+
}, timeoutMs);
|
|
222
|
+
|
|
223
|
+
function finish(err) {
|
|
224
|
+
if (settled) return;
|
|
225
|
+
settled = true;
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
child.kill();
|
|
228
|
+
if (err) reject(err);
|
|
229
|
+
else resolve(stdout);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
child.stdout.on('data', (chunk) => {
|
|
233
|
+
stdout += chunk.toString('utf8');
|
|
234
|
+
if (pattern.test(stdout)) finish();
|
|
235
|
+
});
|
|
236
|
+
child.stderr.on('data', (chunk) => {
|
|
237
|
+
stderr += chunk.toString('utf8');
|
|
238
|
+
});
|
|
239
|
+
child.on('error', finish);
|
|
240
|
+
child.on('exit', (code) => {
|
|
241
|
+
if (!settled && code !== 0) finish(new Error(`eyeling exited with ${code}; stdout=${stdout}; stderr=${stderr}`));
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
164
246
|
const cases = [
|
|
165
247
|
{
|
|
166
248
|
name: 'scoped payload rules run once per RDF Message',
|
|
@@ -220,6 +302,20 @@ const cases = [
|
|
|
220
302
|
}
|
|
221
303
|
},
|
|
222
304
|
},
|
|
305
|
+
{
|
|
306
|
+
name: 'remote HTTP RDF Message Logs emit before the response ends',
|
|
307
|
+
async run(tmp) {
|
|
308
|
+
const rules = path.join(tmp, 'rules.n3');
|
|
309
|
+
writeScopedPayloadRules(rules);
|
|
310
|
+
const server = startOpenEndedMessageServer();
|
|
311
|
+
try {
|
|
312
|
+
const out = await waitForEyelingOutput(['-r', '--stream-messages', rules, server.url], /^one\n/);
|
|
313
|
+
assert.equal(out, 'one\n');
|
|
314
|
+
} finally {
|
|
315
|
+
server.stop();
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
},
|
|
223
319
|
{
|
|
224
320
|
name: 'MARC extraction rules fire over each streamed payload graph',
|
|
225
321
|
run(tmp) {
|
|
@@ -251,7 +347,7 @@ const cases = [
|
|
|
251
347
|
},
|
|
252
348
|
];
|
|
253
349
|
|
|
254
|
-
(function main() {
|
|
350
|
+
(async function main() {
|
|
255
351
|
const suiteStart = msNow();
|
|
256
352
|
info(`Running ${cases.length} stream-message tests`);
|
|
257
353
|
let passed = 0;
|
|
@@ -261,7 +357,7 @@ const cases = [
|
|
|
261
357
|
const testName = numberedName(index, tc.name);
|
|
262
358
|
const start = msNow();
|
|
263
359
|
try {
|
|
264
|
-
tc.run(tmp);
|
|
360
|
+
await tc.run(tmp);
|
|
265
361
|
ok(`${testName} ${C.dim}(${msNow() - start} ms)${C.n}`);
|
|
266
362
|
passed++;
|
|
267
363
|
} catch (e) {
|
|
@@ -281,4 +377,7 @@ const cases = [
|
|
|
281
377
|
}
|
|
282
378
|
fail(`Some stream-message tests failed (${passed}/${cases.length})`);
|
|
283
379
|
process.exit(1);
|
|
284
|
-
})()
|
|
380
|
+
})().catch((e) => {
|
|
381
|
+
fail(e && e.stack ? e.stack : String(e));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
});
|
package/tools/bundle.js
CHANGED
|
@@ -195,7 +195,13 @@ function buildBundleSource({ autoRunMain }) {
|
|
|
195
195
|
out.push(
|
|
196
196
|
' if (__outerModule && __outerRequire && __outerRequire.main === __outerModule && typeof __entry.main === "function") {',
|
|
197
197
|
);
|
|
198
|
-
out.push(' __entry.main();');
|
|
198
|
+
out.push(' const __mainResult = __entry.main();');
|
|
199
|
+
out.push(' if (__mainResult && typeof __mainResult.then === "function") {');
|
|
200
|
+
out.push(' __mainResult.catch((e) => {');
|
|
201
|
+
out.push(' try { if (typeof console !== "undefined" && console.error) console.error(e && e.stack ? e.stack : e && e.message ? e.message : String(e)); } catch (ignoredError) {}');
|
|
202
|
+
out.push(' try { if (typeof process !== "undefined" && process.exit) process.exit(1); } catch (ignoredError) {}');
|
|
203
|
+
out.push(' });');
|
|
204
|
+
out.push(' }');
|
|
199
205
|
out.push(' }');
|
|
200
206
|
out.push(' } catch (ignoredError) {}');
|
|
201
207
|
}
|