eyeling 1.24.30 → 1.24.33
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/dist/browser/eyeling.browser.js +301 -9
- package/examples/deck/rdf-message-flow.md +273 -0
- package/examples/input/rdf-message-flow.trig +56 -105
- package/examples/input/rdf-message-microgrid.trig +39 -72
- package/examples/input/rdf-messages.trig +41 -84
- package/examples/output/rdf-message-flow.md +3 -3
- package/examples/output/rdf-message-microgrid.md +4 -6
- package/examples/output/rdf-messages.md +3 -5
- package/examples/rdf-message-flow.n3 +70 -49
- package/examples/rdf-message-microgrid.n3 +54 -68
- package/examples/rdf-messages.n3 +63 -76
- package/eyeling.js +301 -9
- package/lib/builtins.js +18 -5
- package/lib/lexer.js +283 -4
- package/package.json +1 -1
- package/test/api.test.js +87 -0
|
@@ -1207,25 +1207,36 @@ function parseXsdDateTerm(t) {
|
|
|
1207
1207
|
return d;
|
|
1208
1208
|
}
|
|
1209
1209
|
|
|
1210
|
+
function isXsdDateTimeDatatype(dt) {
|
|
1211
|
+
return dt === XSD_NS + 'dateTime' || dt === XSD_NS + 'dateTimeStamp';
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1210
1214
|
function parseXsdDatetimeTerm(t) {
|
|
1211
1215
|
if (!(t instanceof Literal)) return null;
|
|
1212
1216
|
const [lex, dt] = literalParts(t.value);
|
|
1213
|
-
if (dt
|
|
1217
|
+
if (!isXsdDateTimeDatatype(dt)) return null;
|
|
1214
1218
|
const val = stripQuotes(lex);
|
|
1219
|
+
|
|
1220
|
+
// xsd:dateTimeStamp is a subtype of xsd:dateTime with a required timezone.
|
|
1221
|
+
// Keep xsd:dateTime's existing permissive behaviour, but reject stamp
|
|
1222
|
+
// lexicals that do not actually carry the required timezone.
|
|
1223
|
+
if (dt === XSD_NS + 'dateTimeStamp' && !/(Z|[+-]\d{2}:\d{2})$/.test(val)) return null;
|
|
1224
|
+
|
|
1215
1225
|
const d = new Date(val);
|
|
1216
1226
|
if (Number.isNaN(d.getTime())) return null;
|
|
1217
1227
|
return d; // Date in local/UTC, we only use timestamp
|
|
1218
1228
|
}
|
|
1219
1229
|
|
|
1220
1230
|
function parseXsdDateTimeLexParts(t) {
|
|
1221
|
-
// Parse *lexical* components of an xsd:dateTime literal without timezone normalization.
|
|
1231
|
+
// Parse *lexical* components of an xsd:dateTime/dateTimeStamp literal without timezone normalization.
|
|
1222
1232
|
// Returns { yearStr, month, day, hour, minute, second, tz } or null.
|
|
1223
1233
|
if (!(t instanceof Literal)) return null;
|
|
1224
1234
|
const [lex, dt] = literalParts(t.value);
|
|
1225
|
-
if (dt
|
|
1235
|
+
if (!isXsdDateTimeDatatype(dt)) return null;
|
|
1226
1236
|
const val = stripQuotes(lex);
|
|
1227
1237
|
|
|
1228
1238
|
// xsd:dateTime lexical: YYYY-MM-DDThh:mm:ss(.s+)?(Z|(+|-)hh:mm)?
|
|
1239
|
+
// xsd:dateTimeStamp has the same lexical form, but with the timezone required.
|
|
1229
1240
|
const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
|
|
1230
1241
|
if (!m) return null;
|
|
1231
1242
|
|
|
@@ -1236,6 +1247,7 @@ function parseXsdDateTimeLexParts(t) {
|
|
|
1236
1247
|
const minute = parseInt(m[5], 10);
|
|
1237
1248
|
const second = parseInt(m[6], 10);
|
|
1238
1249
|
const tz = m[7] || null;
|
|
1250
|
+
if (dt === XSD_NS + 'dateTimeStamp' && !tz) return null;
|
|
1239
1251
|
|
|
1240
1252
|
if (!(month >= 1 && month <= 12)) return null;
|
|
1241
1253
|
if (!(day >= 1 && day <= 31)) return null;
|
|
@@ -1302,7 +1314,8 @@ function parseIso8601DurationToSeconds(s) {
|
|
|
1302
1314
|
}
|
|
1303
1315
|
|
|
1304
1316
|
function parseNumericForCompareTerm(t) {
|
|
1305
|
-
// Strict: only accept xsd numeric literals, xsd:duration, xsd:date,
|
|
1317
|
+
// Strict: only accept xsd numeric literals, xsd:duration, xsd:date,
|
|
1318
|
+
// xsd:dateTime, and xsd:dateTimeStamp.
|
|
1306
1319
|
// (or untyped numeric tokens).
|
|
1307
1320
|
const bi = parseIntLiteral(t);
|
|
1308
1321
|
if (bi !== null) return { kind: 'bigint', value: bi };
|
|
@@ -1369,7 +1382,7 @@ function parseNumOrDuration(t) {
|
|
|
1369
1382
|
}
|
|
1370
1383
|
}
|
|
1371
1384
|
|
|
1372
|
-
// xsd:date / xsd:dateTime
|
|
1385
|
+
// xsd:date / xsd:dateTime / xsd:dateTimeStamp
|
|
1373
1386
|
const dtval = parseDatetimeLike(t);
|
|
1374
1387
|
if (dtval !== null) {
|
|
1375
1388
|
return dtval.getTime() / 1000.0;
|
|
@@ -9769,6 +9782,24 @@ function stripQuotes(lex) {
|
|
|
9769
9782
|
// This keeps all downstream parsing/reasoning N3-only.
|
|
9770
9783
|
const LOG_NAME_OF_IRI = '<http://www.w3.org/2000/10/swap/log#nameOf>';
|
|
9771
9784
|
const RDF_REIFIES_IRI = '<http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies>';
|
|
9785
|
+
const RDF_TYPE_IRI = '<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>';
|
|
9786
|
+
const XSD_INTEGER_IRI = '<http://www.w3.org/2001/XMLSchema#integer>';
|
|
9787
|
+
const EYMSG_NS = 'https://eyereasoner.github.io/eyeling/vocab/message#';
|
|
9788
|
+
const EYMSG = Object.freeze({
|
|
9789
|
+
RDFMessageStream: `<${EYMSG_NS}RDFMessageStream>`,
|
|
9790
|
+
MessageEnvelope: `<${EYMSG_NS}MessageEnvelope>`,
|
|
9791
|
+
envelope: `<${EYMSG_NS}envelope>`,
|
|
9792
|
+
firstEnvelope: `<${EYMSG_NS}firstEnvelope>`,
|
|
9793
|
+
lastEnvelope: `<${EYMSG_NS}lastEnvelope>`,
|
|
9794
|
+
orderedEnvelopes: `<${EYMSG_NS}orderedEnvelopes>`,
|
|
9795
|
+
messageCount: `<${EYMSG_NS}messageCount>`,
|
|
9796
|
+
offset: `<${EYMSG_NS}offset>`,
|
|
9797
|
+
nextEnvelope: `<${EYMSG_NS}nextEnvelope>`,
|
|
9798
|
+
payloadGraph: `<${EYMSG_NS}payloadGraph>`,
|
|
9799
|
+
payloadKind: `<${EYMSG_NS}payloadKind>`,
|
|
9800
|
+
empty: `<${EYMSG_NS}empty>`,
|
|
9801
|
+
nonEmpty: `<${EYMSG_NS}nonEmpty>`,
|
|
9802
|
+
});
|
|
9772
9803
|
|
|
9773
9804
|
function normalizeRdfCompatibility(inputText) {
|
|
9774
9805
|
let text = String(inputText ?? '');
|
|
@@ -9779,10 +9810,11 @@ function normalizeRdfCompatibility(inputText) {
|
|
|
9779
9810
|
// plausible top-level TriG named graph block.
|
|
9780
9811
|
const hasTripleTerms = text.includes('<<');
|
|
9781
9812
|
const hasVersionDirective = /^\s*(?:@version|VERSION)\s+(["'])1\.2\1\s*\.?\s*(?:#.*)?$/im.test(text);
|
|
9813
|
+
const hasMessageVersionDirective = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/im.test(text);
|
|
9782
9814
|
const hasNamedGraphCandidate = /(?:^|[.\r\n])\s*(?:GRAPH\s+)?(?:<[^>\r\n]*>|_:[A-Za-z][A-Za-z0-9_-]*|[A-Za-z][A-Za-z0-9_-]*:[^\s{};,.()[\]]*)\s*\{/m.test(text);
|
|
9783
9815
|
const hasAnnotationSyntax = /(?:^|\s)~\s*(?:<|_:[A-Za-z]|[A-Za-z][A-Za-z0-9_-]*:|\{\|)|\{\|/.test(text);
|
|
9784
9816
|
|
|
9785
|
-
if (!hasTripleTerms && !hasVersionDirective && !hasNamedGraphCandidate && !hasAnnotationSyntax) return text;
|
|
9817
|
+
if (!hasTripleTerms && !hasVersionDirective && !hasMessageVersionDirective && !hasNamedGraphCandidate && !hasAnnotationSyntax) return text;
|
|
9786
9818
|
|
|
9787
9819
|
function isWordChar(ch) {
|
|
9788
9820
|
return ch != null && /[A-Za-z0-9_:-]/.test(ch);
|
|
@@ -10136,7 +10168,7 @@ function normalizeRdfCompatibility(inputText) {
|
|
|
10136
10168
|
}
|
|
10137
10169
|
|
|
10138
10170
|
function stripVersionDirectives(s) {
|
|
10139
|
-
return s.replace(/^\s*(?:@version|VERSION)\s+(["'])1\.2
|
|
10171
|
+
return s.replace(/^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)(?:-messages)?\1\s*\.?\s*(?:#.*)?$/gim, '');
|
|
10140
10172
|
}
|
|
10141
10173
|
|
|
10142
10174
|
function skipWsAndComments(s, at) {
|
|
@@ -10290,10 +10322,270 @@ function normalizeRdfCompatibility(inputText) {
|
|
|
10290
10322
|
return out;
|
|
10291
10323
|
}
|
|
10292
10324
|
|
|
10325
|
+
|
|
10326
|
+
function isOnlyWhitespaceAndComments(s) {
|
|
10327
|
+
return skipWsAndComments(s, 0) >= s.length;
|
|
10328
|
+
}
|
|
10329
|
+
|
|
10330
|
+
|
|
10331
|
+
function skipOldStyleDirective(s, at) {
|
|
10332
|
+
let i = at;
|
|
10333
|
+
while (i < s.length) {
|
|
10334
|
+
const ch = s[i];
|
|
10335
|
+
if (ch === '"' || ch === "'") {
|
|
10336
|
+
i = readStringAt(s, i).end;
|
|
10337
|
+
continue;
|
|
10338
|
+
}
|
|
10339
|
+
if (ch === '<') {
|
|
10340
|
+
i = readIriAt(s, i).end;
|
|
10341
|
+
continue;
|
|
10342
|
+
}
|
|
10343
|
+
if (ch === '#') {
|
|
10344
|
+
while (i < s.length && s[i] !== '\n' && s[i] !== '\r') i += 1;
|
|
10345
|
+
continue;
|
|
10346
|
+
}
|
|
10347
|
+
i += 1;
|
|
10348
|
+
if (ch === '.') return i;
|
|
10349
|
+
}
|
|
10350
|
+
return i;
|
|
10351
|
+
}
|
|
10352
|
+
|
|
10353
|
+
function stripDirectivesAndCommentsForEmptiness(s) {
|
|
10354
|
+
let out = '';
|
|
10355
|
+
let i = 0;
|
|
10356
|
+
let statementStart = true;
|
|
10357
|
+
while (i < s.length) {
|
|
10358
|
+
const ch = s[i];
|
|
10359
|
+
if (ch === '"' || ch === "'") {
|
|
10360
|
+
const str = readStringAt(s, i);
|
|
10361
|
+
out += str.text;
|
|
10362
|
+
i = str.end;
|
|
10363
|
+
statementStart = false;
|
|
10364
|
+
continue;
|
|
10365
|
+
}
|
|
10366
|
+
if (ch === '<') {
|
|
10367
|
+
const iri = readIriAt(s, i);
|
|
10368
|
+
out += iri.text;
|
|
10369
|
+
i = iri.end;
|
|
10370
|
+
statementStart = false;
|
|
10371
|
+
continue;
|
|
10372
|
+
}
|
|
10373
|
+
if (ch === '#') {
|
|
10374
|
+
while (i < s.length && s[i] !== '\n' && s[i] !== '\r') i += 1;
|
|
10375
|
+
statementStart = true;
|
|
10376
|
+
continue;
|
|
10377
|
+
}
|
|
10378
|
+
if (statementStart) {
|
|
10379
|
+
const start = skipWsAndComments(s, i);
|
|
10380
|
+
out += s.slice(i, start);
|
|
10381
|
+
i = start;
|
|
10382
|
+
if (startsWordAt(s, 'PREFIX', i) || startsWordAt(s, 'BASE', i) || startsWordAt(s, 'VERSION', i)) {
|
|
10383
|
+
while (i < s.length && s[i] !== '\n' && s[i] !== '\r') i += 1;
|
|
10384
|
+
statementStart = true;
|
|
10385
|
+
continue;
|
|
10386
|
+
}
|
|
10387
|
+
if (s[i] === '@') {
|
|
10388
|
+
const lower = s.slice(i, i + 9).toLowerCase();
|
|
10389
|
+
if (lower.startsWith('@prefix') || lower.startsWith('@base') || lower.startsWith('@version')) {
|
|
10390
|
+
i = skipOldStyleDirective(s, i);
|
|
10391
|
+
statementStart = true;
|
|
10392
|
+
continue;
|
|
10393
|
+
}
|
|
10394
|
+
}
|
|
10395
|
+
}
|
|
10396
|
+
out += ch;
|
|
10397
|
+
if (ch === '.' || ch === '}' || ch === '\n' || ch === '\r') statementStart = true;
|
|
10398
|
+
else if (!/\s/.test(ch)) statementStart = false;
|
|
10399
|
+
i += 1;
|
|
10400
|
+
}
|
|
10401
|
+
return out;
|
|
10402
|
+
}
|
|
10403
|
+
|
|
10404
|
+
function simpleHashText(s) {
|
|
10405
|
+
let h = 0x811c9dc5;
|
|
10406
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
10407
|
+
h ^= s.charCodeAt(i);
|
|
10408
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
10409
|
+
}
|
|
10410
|
+
return h.toString(16).padStart(8, '0');
|
|
10411
|
+
}
|
|
10412
|
+
|
|
10413
|
+
function rewriteMessageBlankLabels(s, messageIndex) {
|
|
10414
|
+
let out = '';
|
|
10415
|
+
let i = 0;
|
|
10416
|
+
const prefix = `_:eyeling_m${String(messageIndex).padStart(3, '0')}_`;
|
|
10417
|
+
while (i < s.length) {
|
|
10418
|
+
const ch = s[i];
|
|
10419
|
+
if (ch === '"' || ch === "'") {
|
|
10420
|
+
const str = readStringAt(s, i);
|
|
10421
|
+
out += str.text;
|
|
10422
|
+
i = str.end;
|
|
10423
|
+
continue;
|
|
10424
|
+
}
|
|
10425
|
+
if (ch === '<' && !s.startsWith('<<', i)) {
|
|
10426
|
+
const iri = readIriAt(s, i);
|
|
10427
|
+
out += iri.text;
|
|
10428
|
+
i = iri.end;
|
|
10429
|
+
continue;
|
|
10430
|
+
}
|
|
10431
|
+
if (ch === '#') {
|
|
10432
|
+
while (i < s.length) {
|
|
10433
|
+
const c = s[i++];
|
|
10434
|
+
out += c;
|
|
10435
|
+
if (c === '\n' || c === '\r') break;
|
|
10436
|
+
}
|
|
10437
|
+
continue;
|
|
10438
|
+
}
|
|
10439
|
+
if (s.startsWith('_:', i)) {
|
|
10440
|
+
let j = i + 2;
|
|
10441
|
+
while (j < s.length && !/\s/.test(s[j]) && !'{}[](),;.'.includes(s[j])) j += 1;
|
|
10442
|
+
const label = s.slice(i + 2, j);
|
|
10443
|
+
if (label) {
|
|
10444
|
+
out += prefix + label.replace(/[^A-Za-z0-9_]/g, '_');
|
|
10445
|
+
i = j;
|
|
10446
|
+
continue;
|
|
10447
|
+
}
|
|
10448
|
+
}
|
|
10449
|
+
out += ch;
|
|
10450
|
+
i += 1;
|
|
10451
|
+
}
|
|
10452
|
+
return out;
|
|
10453
|
+
}
|
|
10454
|
+
|
|
10455
|
+
function findMessageDirectiveAt(s, at) {
|
|
10456
|
+
if (startsWordAt(s, 'MESSAGE', at)) return { start: at, end: at + 'MESSAGE'.length };
|
|
10457
|
+
if (s.slice(at, at + 8).toLowerCase() === '@message' && !isWordChar(s[at + 8])) {
|
|
10458
|
+
let end = at + 8;
|
|
10459
|
+
end = skipWsAndComments(s, end);
|
|
10460
|
+
if (s[end] === '.') end += 1;
|
|
10461
|
+
return { start: at, end };
|
|
10462
|
+
}
|
|
10463
|
+
return null;
|
|
10464
|
+
}
|
|
10465
|
+
|
|
10466
|
+
function splitRdfMessageLog(s) {
|
|
10467
|
+
const chunks = [];
|
|
10468
|
+
let i = 0;
|
|
10469
|
+
let start = 0;
|
|
10470
|
+
let braceDepth = 0;
|
|
10471
|
+
let bracketDepth = 0;
|
|
10472
|
+
let parenDepth = 0;
|
|
10473
|
+
let statementStart = true;
|
|
10474
|
+
let sawDelimiter = false;
|
|
10475
|
+
|
|
10476
|
+
while (i < s.length) {
|
|
10477
|
+
const ch = s[i];
|
|
10478
|
+
if (ch === '"' || ch === "'") {
|
|
10479
|
+
i = readStringAt(s, i).end;
|
|
10480
|
+
statementStart = false;
|
|
10481
|
+
continue;
|
|
10482
|
+
}
|
|
10483
|
+
if (ch === '<' && !s.startsWith('<<', i)) {
|
|
10484
|
+
i = readIriAt(s, i).end;
|
|
10485
|
+
statementStart = false;
|
|
10486
|
+
continue;
|
|
10487
|
+
}
|
|
10488
|
+
if (ch === '#') {
|
|
10489
|
+
while (i < s.length && s[i] !== '\n' && s[i] !== '\r') i += 1;
|
|
10490
|
+
statementStart = true;
|
|
10491
|
+
continue;
|
|
10492
|
+
}
|
|
10493
|
+
if (statementStart && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
|
|
10494
|
+
const termStart = skipWsAndComments(s, i);
|
|
10495
|
+
const msg = findMessageDirectiveAt(s, termStart);
|
|
10496
|
+
if (msg) {
|
|
10497
|
+
chunks.push(s.slice(start, termStart));
|
|
10498
|
+
start = msg.end;
|
|
10499
|
+
i = msg.end;
|
|
10500
|
+
statementStart = true;
|
|
10501
|
+
sawDelimiter = true;
|
|
10502
|
+
continue;
|
|
10503
|
+
}
|
|
10504
|
+
if (termStart !== i) {
|
|
10505
|
+
i = termStart;
|
|
10506
|
+
continue;
|
|
10507
|
+
}
|
|
10508
|
+
}
|
|
10509
|
+
if (ch === '{') braceDepth += 1;
|
|
10510
|
+
else if (ch === '}' && braceDepth > 0) braceDepth -= 1;
|
|
10511
|
+
else if (ch === '[') bracketDepth += 1;
|
|
10512
|
+
else if (ch === ']' && bracketDepth > 0) bracketDepth -= 1;
|
|
10513
|
+
else if (ch === '(') parenDepth += 1;
|
|
10514
|
+
else if (ch === ')' && parenDepth > 0) parenDepth -= 1;
|
|
10515
|
+
|
|
10516
|
+
if (ch === '.' && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) statementStart = true;
|
|
10517
|
+
else if (ch === '\n' || ch === '\r') statementStart = true;
|
|
10518
|
+
else if (!/\s/.test(ch)) statementStart = false;
|
|
10519
|
+
i += 1;
|
|
10520
|
+
}
|
|
10521
|
+
|
|
10522
|
+
const tail = s.slice(start);
|
|
10523
|
+
if (!sawDelimiter || !isOnlyWhitespaceAndComments(tail)) chunks.push(tail);
|
|
10524
|
+
return chunks;
|
|
10525
|
+
}
|
|
10526
|
+
|
|
10527
|
+
function normalizeMessageChunk(chunk, messageIndex) {
|
|
10528
|
+
let body = String(chunk || '');
|
|
10529
|
+
if (hasTripleTerms || body.includes('<<')) body = convertTripleTerms(body);
|
|
10530
|
+
if (hasAnnotationSyntax || /(?:^|\s)~\s*(?:<|_:[A-Za-z]|[A-Za-z][A-Za-z0-9_-]*:|\{\|)|\{\|/.test(body)) {
|
|
10531
|
+
body = convertAnnotations(body);
|
|
10532
|
+
}
|
|
10533
|
+
body = normalizeNamedGraphs(body);
|
|
10534
|
+
body = rewriteMessageBlankLabels(body, messageIndex);
|
|
10535
|
+
return body.trim();
|
|
10536
|
+
}
|
|
10537
|
+
|
|
10538
|
+
function messageChunkHasRdf(body) {
|
|
10539
|
+
return !isOnlyWhitespaceAndComments(stripDirectivesAndCommentsForEmptiness(body));
|
|
10540
|
+
}
|
|
10541
|
+
|
|
10542
|
+
function normalizeRdfMessageLog(s) {
|
|
10543
|
+
const withoutVersion = stripVersionDirectives(s);
|
|
10544
|
+
const chunks = splitRdfMessageLog(withoutVersion);
|
|
10545
|
+
const hash = simpleHashText(s);
|
|
10546
|
+
const base = `urn:eyeling:message-log:${hash}`;
|
|
10547
|
+
const stream = `<${base}#stream>`;
|
|
10548
|
+
const envelopeIris = chunks.map((unused, idx) => `<${base}#m${String(idx + 1).padStart(3, '0')}>`);
|
|
10549
|
+
const payloadIris = chunks.map((unused, idx) => `<${base}#m${String(idx + 1).padStart(3, '0')}/payload>`);
|
|
10550
|
+
const out = [];
|
|
10551
|
+
|
|
10552
|
+
out.push(`${stream} ${RDF_TYPE_IRI} ${EYMSG.RDFMessageStream} .`);
|
|
10553
|
+
out.push(`${stream} ${EYMSG.messageCount} "${chunks.length}"^^${XSD_INTEGER_IRI} .`);
|
|
10554
|
+
if (envelopeIris.length) {
|
|
10555
|
+
out.push(`${stream} ${EYMSG.orderedEnvelopes} (${envelopeIris.join(' ')}) .`);
|
|
10556
|
+
out.push(`${stream} ${EYMSG.firstEnvelope} ${envelopeIris[0]} .`);
|
|
10557
|
+
out.push(`${stream} ${EYMSG.lastEnvelope} ${envelopeIris[envelopeIris.length - 1]} .`);
|
|
10558
|
+
}
|
|
10559
|
+
|
|
10560
|
+
for (let idx = 0; idx < chunks.length; idx += 1) {
|
|
10561
|
+
const n = idx + 1;
|
|
10562
|
+
const envelope = envelopeIris[idx];
|
|
10563
|
+
const payload = payloadIris[idx];
|
|
10564
|
+
const body = normalizeMessageChunk(chunks[idx], n);
|
|
10565
|
+
const hasBody = messageChunkHasRdf(body);
|
|
10566
|
+
|
|
10567
|
+
out.push(`${stream} ${EYMSG.envelope} ${envelope} .`);
|
|
10568
|
+
out.push(`${envelope} ${RDF_TYPE_IRI} ${EYMSG.MessageEnvelope} .`);
|
|
10569
|
+
out.push(`${envelope} ${EYMSG.offset} "${n}"^^${XSD_INTEGER_IRI} .`);
|
|
10570
|
+
out.push(`${envelope} ${EYMSG.payloadKind} ${hasBody ? EYMSG.nonEmpty : EYMSG.empty} .`);
|
|
10571
|
+
if (idx + 1 < envelopeIris.length) out.push(`${envelope} ${EYMSG.nextEnvelope} ${envelopeIris[idx + 1]} .`);
|
|
10572
|
+
if (hasBody) {
|
|
10573
|
+
out.push(`${envelope} ${EYMSG.payloadGraph} ${payload} .`);
|
|
10574
|
+
out.push(`${payload} ${LOG_NAME_OF_IRI} {`);
|
|
10575
|
+
out.push(body);
|
|
10576
|
+
out.push(`} .`);
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
|
|
10580
|
+
return out.join('\n') + '\n';
|
|
10581
|
+
}
|
|
10582
|
+
|
|
10583
|
+
if (hasMessageVersionDirective) return normalizeRdfMessageLog(text);
|
|
10584
|
+
|
|
10293
10585
|
if (hasTripleTerms) text = convertTripleTerms(text);
|
|
10294
10586
|
if (hasAnnotationSyntax) text = convertAnnotations(text);
|
|
10295
|
-
if (hasVersionDirective) text = stripVersionDirectives(text);
|
|
10296
|
-
if (hasVersionDirective || hasNamedGraphCandidate) text = normalizeNamedGraphs(text);
|
|
10587
|
+
if (hasVersionDirective || hasMessageVersionDirective) text = stripVersionDirectives(text);
|
|
10588
|
+
if (hasVersionDirective || hasMessageVersionDirective || hasNamedGraphCandidate) text = normalizeNamedGraphs(text);
|
|
10297
10589
|
return text;
|
|
10298
10590
|
}
|
|
10299
10591
|
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# RDF Message Logs in Eyeling — from stream to reasoning
|
|
2
|
+
|
|
3
|
+
This deck explains the example `rdf-message-flow.n3` and its input file `input/rdf-message-flow.trig`.
|
|
4
|
+
|
|
5
|
+
The goal is to show, in plain language, how Eyeling can now read an RDF Message Log directly instead of asking the example data to describe its own message envelopes by hand.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The everyday problem
|
|
10
|
+
|
|
11
|
+
Many real systems do not receive one big dataset.
|
|
12
|
+
|
|
13
|
+
They receive a stream of small updates:
|
|
14
|
+
|
|
15
|
+
- a sensor reading,
|
|
16
|
+
- a command,
|
|
17
|
+
- a status heartbeat,
|
|
18
|
+
- an alert,
|
|
19
|
+
- another sensor reading.
|
|
20
|
+
|
|
21
|
+
Each update matters as a separate communication event.
|
|
22
|
+
|
|
23
|
+
If we simply merge everything into one graph, we lose the order and the boundary between messages.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## A message is a sealed packet
|
|
28
|
+
|
|
29
|
+
Think of an RDF Message as a sealed packet of RDF data.
|
|
30
|
+
|
|
31
|
+
Inside the packet there may be triples or named graphs.
|
|
32
|
+
|
|
33
|
+
Outside the packet there is the stream order: first message, second message, third message, and so on.
|
|
34
|
+
|
|
35
|
+
The important idea is:
|
|
36
|
+
|
|
37
|
+
> The reasoner should know when one message ends and the next one begins.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## What an RDF Message Log adds
|
|
42
|
+
|
|
43
|
+
An RDF Message Log is a replayable record of a message stream.
|
|
44
|
+
|
|
45
|
+
Instead of saying “subscribe to this live channel”, the file says:
|
|
46
|
+
|
|
47
|
+
> Here are the messages that arrived, in order.
|
|
48
|
+
|
|
49
|
+
That makes it useful for examples, tests, audits, debugging, reproducible reasoning, and explanations.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## The new syntax in the input file
|
|
54
|
+
|
|
55
|
+
The input begins with:
|
|
56
|
+
|
|
57
|
+
```trig
|
|
58
|
+
VERSION "1.2-messages"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That tells Eyeling:
|
|
62
|
+
|
|
63
|
+
> This file contains message boundaries.
|
|
64
|
+
|
|
65
|
+
Then each boundary is written as:
|
|
66
|
+
|
|
67
|
+
```trig
|
|
68
|
+
MESSAGE
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
So the file can look like this:
|
|
72
|
+
|
|
73
|
+
```trig
|
|
74
|
+
# message 1 data
|
|
75
|
+
:temperatureFlow :highThreshold 26 .
|
|
76
|
+
_:obs sosa:hasSimpleResult 21 .
|
|
77
|
+
|
|
78
|
+
MESSAGE
|
|
79
|
+
|
|
80
|
+
# message 2 data
|
|
81
|
+
_:obs sosa:hasSimpleResult 22 .
|
|
82
|
+
|
|
83
|
+
MESSAGE
|
|
84
|
+
|
|
85
|
+
# message 3: empty heartbeat
|
|
86
|
+
MESSAGE
|
|
87
|
+
|
|
88
|
+
# message 4 data
|
|
89
|
+
_:obs sosa:hasSimpleResult 28 .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## What Eyeling does internally
|
|
95
|
+
|
|
96
|
+
Eyeling does not treat `MESSAGE` as an ordinary RDF term.
|
|
97
|
+
|
|
98
|
+
It handles it before normal N3 reasoning starts.
|
|
99
|
+
|
|
100
|
+
Internally, Eyeling turns the log into a replay view:
|
|
101
|
+
|
|
102
|
+
- one stream resource,
|
|
103
|
+
- one envelope per message,
|
|
104
|
+
- an offset for each envelope,
|
|
105
|
+
- a link to the next envelope,
|
|
106
|
+
- a payload graph for each non-empty message,
|
|
107
|
+
- and an explicit marker for empty messages.
|
|
108
|
+
|
|
109
|
+
The rules then reason over that replay view.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Why this is better than hand-written envelopes
|
|
114
|
+
|
|
115
|
+
Before this change, the example input had to describe the stream manually:
|
|
116
|
+
|
|
117
|
+
- message `:m001`,
|
|
118
|
+
- message `:m002`,
|
|
119
|
+
- payload graph `in:payload001`,
|
|
120
|
+
- next message links,
|
|
121
|
+
- payload kind markers,
|
|
122
|
+
- offsets.
|
|
123
|
+
|
|
124
|
+
That worked, but it made the example bulky.
|
|
125
|
+
|
|
126
|
+
It also mixed two concerns:
|
|
127
|
+
|
|
128
|
+
1. the message-log machinery, and
|
|
129
|
+
2. the domain logic of routing temperature observations.
|
|
130
|
+
|
|
131
|
+
Now Eyeling handles the message-log machinery.
|
|
132
|
+
|
|
133
|
+
The N3 file can focus on the logic.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## What the temperature-flow example does
|
|
138
|
+
|
|
139
|
+
The example models a small stream processor.
|
|
140
|
+
|
|
141
|
+
Messages move through these stages:
|
|
142
|
+
|
|
143
|
+
1. ingest,
|
|
144
|
+
2. validate,
|
|
145
|
+
3. interpret,
|
|
146
|
+
4. route,
|
|
147
|
+
5. sink.
|
|
148
|
+
|
|
149
|
+
The stream contains temperature readings and one empty heartbeat.
|
|
150
|
+
|
|
151
|
+
The rules route normal readings to an archive sink and high readings to an alert sink.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## The empty heartbeat matters
|
|
156
|
+
|
|
157
|
+
One message in the example contains no RDF triples.
|
|
158
|
+
|
|
159
|
+
That is not an error.
|
|
160
|
+
|
|
161
|
+
It represents a heartbeat:
|
|
162
|
+
|
|
163
|
+
> “The stream is still alive, even though there is no new observation payload.”
|
|
164
|
+
|
|
165
|
+
Eyeling still creates an envelope for it.
|
|
166
|
+
|
|
167
|
+
That means the empty message keeps its place in the ordered replay.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Blank nodes stay message-local
|
|
172
|
+
|
|
173
|
+
The input deliberately reuses the same blank-node label in several messages:
|
|
174
|
+
|
|
175
|
+
```trig
|
|
176
|
+
_:obs sosa:hasSimpleResult 21 .
|
|
177
|
+
|
|
178
|
+
MESSAGE
|
|
179
|
+
|
|
180
|
+
_:obs sosa:hasSimpleResult 22 .
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
That does not mean both messages talk about the same blank node.
|
|
184
|
+
|
|
185
|
+
In a message log, blank-node labels are scoped to the message.
|
|
186
|
+
|
|
187
|
+
Eyeling rewrites them internally so each message gets its own blank nodes.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## How the N3 rules see the replay
|
|
192
|
+
|
|
193
|
+
The N3 rules do not see `MESSAGE` directly.
|
|
194
|
+
|
|
195
|
+
They see Eyeling’s replay vocabulary, `eymsg:`.
|
|
196
|
+
|
|
197
|
+
For example, a rule can ask:
|
|
198
|
+
|
|
199
|
+
```n3
|
|
200
|
+
?Stream a eymsg:RDFMessageStream;
|
|
201
|
+
eymsg:firstEnvelope ?Envelope.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Another rule can inspect a payload:
|
|
205
|
+
|
|
206
|
+
```n3
|
|
207
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
208
|
+
?Payload log:nameOf ?PayloadContext.
|
|
209
|
+
?PayloadContext log:includes {
|
|
210
|
+
?Observation sosa:hasSimpleResult ?Result.
|
|
211
|
+
}.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
That keeps each message payload inside its own context.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Back pressure in one sentence
|
|
219
|
+
|
|
220
|
+
The example releases only the first envelope at the start.
|
|
221
|
+
|
|
222
|
+
Each envelope must reach the sink before the next envelope is released.
|
|
223
|
+
|
|
224
|
+
That gives a simple form of ordered replay or back pressure:
|
|
225
|
+
|
|
226
|
+
> process this message, then release the next one.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## What the final answer says
|
|
231
|
+
|
|
232
|
+
When the example succeeds, Eyeling reports that five parser-replayed envelopes moved through the flow.
|
|
233
|
+
|
|
234
|
+
With a threshold of 26:
|
|
235
|
+
|
|
236
|
+
- 21 is archived,
|
|
237
|
+
- 22 is archived,
|
|
238
|
+
- the empty heartbeat is accepted,
|
|
239
|
+
- 28 becomes an alert,
|
|
240
|
+
- 29 becomes an alert.
|
|
241
|
+
|
|
242
|
+
The important part is not only the routing result.
|
|
243
|
+
|
|
244
|
+
The important part is that the result was derived while preserving message boundaries.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Why a wide audience should care
|
|
249
|
+
|
|
250
|
+
This pattern is useful wherever data arrives over time:
|
|
251
|
+
|
|
252
|
+
- sensors,
|
|
253
|
+
- event logs,
|
|
254
|
+
- audit trails,
|
|
255
|
+
- clinical systems,
|
|
256
|
+
- energy systems,
|
|
257
|
+
- pub/sub channels,
|
|
258
|
+
- digital twins,
|
|
259
|
+
- provenance streams.
|
|
260
|
+
|
|
261
|
+
You can replay the stream, reason over each message atomically, and explain what happened without flattening the whole history into one graph.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## The takeaway
|
|
266
|
+
|
|
267
|
+
`MESSAGE` is the boundary.
|
|
268
|
+
|
|
269
|
+
Eyeling turns those boundaries into ordered replay envelopes.
|
|
270
|
+
|
|
271
|
+
The N3 rules consume the replay and focus on the domain logic.
|
|
272
|
+
|
|
273
|
+
That makes the example shorter, clearer, and closer to how a real pub/sub channel would be processed.
|