@factoidal/core 0.18.0 → 0.20.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/lib/api.mjs CHANGED
@@ -26,7 +26,7 @@ import { dataFactory, termFromSrj, termToJson, quadToJson, _setTextEngine } from
26
26
  import { Dataset, INTERNAL, checkOptions, makeRegistry, WRITER_TAGS } from './dataset.mjs';
27
27
  import { version as packageVersion } from './version.mjs';
28
28
  import { openGeneration, readRegion, manifestKeys, hexOfBytes } from './store.mjs';
29
- import { fullEngineMessage } from './lean-entry.mjs';
29
+ import { fullEngineMessage, encodeJsValue } from './lean-entry.mjs';
30
30
  import { memoryDeltaLogStore } from './delta-log-hosts.mjs';
31
31
  import { FactoidalError, errorFromEnvelope, envelopeResult } from './errors.mjs';
32
32
  import { buildNodes } from './nodes.mjs';
@@ -735,16 +735,33 @@ function buildApi(driver) {
735
735
  * skipped statement in `dataset.diagnostics`
736
736
  * (https://github.com/danbri/factoidal/issues/716); `maxErrors: n` stops
737
737
  * after n diagnostics. With no `format` the engine infers one
738
- * (`dataset.format`, `dataset.formatInferred`).
738
+ * (`dataset.format`, `dataset.formatInferred`). `{rdfstar:true}` reads
739
+ * JSON-LD as JSON-LD-star (`@triple`, `@reifies`, `@annotation`), as
740
+ * jsonldToRdf() does; it applies to format 'jsonld' only.
739
741
  * @param {string} text
740
742
  * @param {{format?: string, baseIRI?: string, lenient?: boolean,
741
- * maxErrors?: number}} [options]
743
+ * maxErrors?: number, rdfstar?: boolean}} [options]
742
744
  * @returns {Promise<Dataset>}
743
745
  */
744
746
  async function parse(text, options) {
745
747
  if (typeof text !== 'string') throw new TypeError('parse: text must be a string');
746
748
  const opts = await checkedOptions('parse', optionsOf('parse', options));
747
749
  const c = await ctx();
750
+ if (opts.rdfstar) {
751
+ const { tag, inferred } = formatTagOf(c, text, opts.format, 'parse');
752
+ if (tag !== 'jsonld') {
753
+ throw new FactoidalError(
754
+ `parse: rdfstar applies to format 'jsonld' only (format '${tag}'${inferred ? ', inferred' : ''})`,
755
+ { code: 'option.conflict' });
756
+ }
757
+ requireEntryFn(c.e, 'datasetFromJsonLd', 'parse {rdfstar:true} (datasetFromJsonLd)');
758
+ const optionsJson = JSON.stringify({
759
+ ...(opts.baseIRI ? { base: opts.baseIRI } : {}),
760
+ rdfstar: true,
761
+ });
762
+ return c.wrap(entryResult(c.e.datasetFromJsonLd(text, optionsJson), 'parse'), {},
763
+ { format: tag, formatInferred: inferred });
764
+ }
748
765
  if (opts.lenient) {
749
766
  const { tag, inferred } = formatTagOf(c, text, opts.format, 'parse');
750
767
  requireEntryFn(c.e, 'parseDocument', 'parse {lenient:true} (parseDocument)');
@@ -885,30 +902,43 @@ function buildApi(driver) {
885
902
  }
886
903
 
887
904
  /**
888
- * The RDFC-1.0 canonical hash of one graph: the SHA-256, as 64 lowercase
889
- * hexadecimal digits, of the canonical N-Quads document (RDF Dataset
890
- * Canonicalization 1.0, section 4.4 note 2). Every quad's graph name is
891
- * dropped first (op datasetGraphAsDefault with the union of the graphs);
892
- * then op datasetCanonicalHash computes the canonical form and the digest
893
- * in Lean. canonicalize() answers the canonical text.
894
- * @param {Dataset} datasetOrGraph
905
+ * The RDFC-1.0 canonical hash of a dataset: the SHA-256, as 64 lowercase
906
+ * hexadecimal digits, of the canonical N-Quads document that canonicalize()
907
+ * answers (RDF Dataset Canonicalization 1.0, section 4.4 note 2), graph
908
+ * names included. Op datasetCanonicalHash computes the canonical form and
909
+ * the digest in Lean. Since 0.20.0; 0.17.0 to 0.19.x dropped every graph
910
+ * name first, so datasets that differ only in their graph names got the
911
+ * same hash. To hash a graph whatever its name, use
912
+ * canonicalHash(asDefaultGraph(g)).
913
+ * @param {Dataset} dataset
895
914
  * @returns {Promise<string>}
896
915
  */
897
- async function canonicalHash(datasetOrGraph) {
898
- if (!(datasetOrGraph instanceof Dataset)) {
899
- throw new TypeError('canonicalHash: expected a Dataset (e.g. one entry of graphs())');
916
+ async function canonicalHash(dataset) {
917
+ if (!(dataset instanceof Dataset)) {
918
+ throw new TypeError('canonicalHash: expected a Dataset');
900
919
  }
901
- datasetOrGraph.size; // throws the disposed error
902
- const c = datasetOrGraph._ctx;
903
- requireEntryFn(c.e, 'datasetGraphAsDefault', 'canonicalHash');
920
+ dataset.size; // throws the disposed error
921
+ const c = dataset._ctx;
904
922
  requireEntryFn(c.e, 'datasetCanonicalHash', 'canonicalHash');
905
- const merged = c.wrap(entryResult(c.e.datasetGraphAsDefault(datasetOrGraph.handle,
906
- JSON.stringify({ type: 'union' })), 'canonicalHash'), {});
907
- try {
908
- return entryResult(c.e.datasetCanonicalHash(merged.handle), 'canonicalHash').hash;
909
- } finally {
910
- merged.dispose();
923
+ return entryResult(c.e.datasetCanonicalHash(dataset.handle), 'canonicalHash').hash;
924
+ }
925
+
926
+ /**
927
+ * A new Dataset that holds the triples of every graph of `dataset`, all in
928
+ * the default graph (op datasetGraphAsDefault with the union of the
929
+ * graphs). With one entry of graphs(), it is that graph without its name.
930
+ * @param {Dataset} dataset
931
+ * @returns {Dataset}
932
+ */
933
+ function asDefaultGraph(dataset) {
934
+ if (!(dataset instanceof Dataset)) {
935
+ throw new TypeError('asDefaultGraph: expected a Dataset');
911
936
  }
937
+ dataset.size; // throws the disposed error
938
+ const c = dataset._ctx;
939
+ requireEntryFn(c.e, 'datasetGraphAsDefault', 'asDefaultGraph');
940
+ return c.wrap(entryResult(c.e.datasetGraphAsDefault(dataset.handle,
941
+ JSON.stringify({ type: 'union' })), 'asDefaultGraph'), {});
912
942
  }
913
943
 
914
944
  /**
@@ -949,6 +979,95 @@ function buildApi(driver) {
949
979
  await ctx();
950
980
  }
951
981
 
982
+ // The synchronous term functions (sameValue, syntaxFor, splitIri, ...) call
983
+ // the loaded engine: `await ready()`, or any other call, loads it first.
984
+ function loadedCtx(who, op = who) {
985
+ if (!ctxCache) {
986
+ throw new FactoidalError(
987
+ `${who}: the engine is not loaded yet. Await any @factoidal/core call first ` +
988
+ '(for example `await ready()`).', { code: 'op.unknown' });
989
+ }
990
+ requireEntryFn(ctxCache.e, op, who);
991
+ return ctxCache;
992
+ }
993
+
994
+ /**
995
+ * SPARQL 1.1 `=` on two RDF terms (sections 17.3 and 17.4.1.7), decided by
996
+ * the engine (L4Factoidal.SPARQL.sameValue;
997
+ * https://github.com/danbri/factoidal/issues/1052).
998
+ * @param {object|string} a an RDF/JS term or an N-Triples term string
999
+ * @param {object|string} b
1000
+ * @returns {boolean|null} true: the same value; false: different values;
1001
+ * null: incomparable (the operator's type error)
1002
+ */
1003
+ function sameValue(a, b) {
1004
+ const c = loadedCtx('sameValue');
1005
+ const r = entryResult(c.e.sameValue(JSON.stringify(termToJson(a)),
1006
+ JSON.stringify(termToJson(b))), 'sameValue');
1007
+ return r.sameValue === 'true' ? true : (r.sameValue === 'false' ? false : null);
1008
+ }
1009
+
1010
+ const iriText = (iri) => (typeof iri === 'string' ? iri : (iri && iri.value) || '');
1011
+
1012
+ /**
1013
+ * The syntax of a document by its file name (or IRI path) and its media
1014
+ * type, from the engine's table of W3C registrations
1015
+ * (L4Factoidal.RDF.syntaxOfNames; https://github.com/danbri/factoidal/issues/1053).
1016
+ * The media type wins when it is given; an unknown media type does not fall
1017
+ * back to the name.
1018
+ * @param {string|{fileName?: string, mediaType?: string}} what a file name or IRI,
1019
+ * or both names
1020
+ * @returns {string|null} a syntax name of formats(), or null
1021
+ */
1022
+ function syntaxFor(what) {
1023
+ const c = loadedCtx('syntaxFor');
1024
+ const o = (typeof what === 'string') ? { fileName: what } : (what || {});
1025
+ const r = entryResult(c.e.syntaxFor(String(o.fileName ?? ''), String(o.mediaType ?? '')), 'syntaxFor');
1026
+ return r.syntax ?? null;
1027
+ }
1028
+
1029
+ /**
1030
+ * An IRI as a namespace and a local name: the local name is the longest
1031
+ * suffix that is an XML NCName, '' when there is none
1032
+ * (L4Factoidal.RDF.splitIriXml; https://github.com/danbri/factoidal/issues/1051 item 4).
1033
+ * @param {string|{value: string}} iri
1034
+ * @returns {{namespace: string, localName: string}}
1035
+ */
1036
+ function splitIri(iri) {
1037
+ const c = loadedCtx('splitIri');
1038
+ const r = entryResult(c.e.splitIri(iriText(iri)), 'splitIri');
1039
+ return { namespace: r.namespace, localName: r.localName };
1040
+ }
1041
+
1042
+ /** Is the text an XML NCName (Namespaces in XML 1.0, production [4])? */
1043
+ function isNCName(text) {
1044
+ const c = loadedCtx('isNCName');
1045
+ return entryResult(c.e.isNCName(String(text)), 'isNCName').ncName === true;
1046
+ }
1047
+
1048
+ /**
1049
+ * The engine's vocabulary table: prefix, namespace and term local names of
1050
+ * rdf, rdfs, xsd and owl, and the namespaces of foaf, dcterms, dc and schema
1051
+ * (L4Factoidal.RDF.vocabularyTable; https://github.com/danbri/factoidal/issues/1051 item 5).
1052
+ * @returns {Array<{prefix: string, namespace: string, terms: string[]}>}
1053
+ */
1054
+ function vocabularies() {
1055
+ const c = loadedCtx('vocabularies');
1056
+ return entryResult(c.e.vocabularies(), 'vocabularies').vocabularies;
1057
+ }
1058
+
1059
+ /** The container membership property rdf:_n, n >= 1, as a NamedNode. */
1060
+ function memberIri(n) {
1061
+ const c = loadedCtx('memberIri');
1062
+ return dataFactory.namedNode(entryResult(c.e.memberIri(String(n)), 'memberIri').iri);
1063
+ }
1064
+
1065
+ /** The n of rdf:_n, or null for any other IRI. */
1066
+ function memberOrdinal(iri) {
1067
+ const c = loadedCtx('memberOrdinal');
1068
+ return entryResult(c.e.memberOrdinal(iriText(iri)), 'memberOrdinal').ordinal ?? null;
1069
+ }
1070
+
952
1071
  /** The engine's name tables (formats, regimes, query outputs, pack syntaxes). */
953
1072
  async function engineTables() {
954
1073
  return (await ctx()).tables();
@@ -2198,13 +2317,110 @@ function buildApi(driver) {
2198
2317
  turtlePrefixes: has('serializeTurtleWith'),
2199
2318
  store: has('storeOpen') && has('storeHandleQuery'),
2200
2319
  deltaLog: has('deltaLogEncode') && has('deltaLogDecode') && has('deltaLogMerge'),
2320
+ literalValues: has('literalValue') && has('literalFromValue'),
2201
2321
  };
2202
2322
  }
2203
2323
 
2324
+ // ---------------------------------------------------------------
2325
+ // Literal values (https://github.com/danbri/factoidal/issues/1051 items 1-3).
2326
+ // The ops literalValue and literalFromValue (formal/lean4/Wasm/Ops/
2327
+ // LiteralValue.lean, over L4Factoidal.RDF.LiteralValue) make every
2328
+ // decision: the XSD lexical-to-value mapping, validity, the canonical form,
2329
+ // the datatype of a JavaScript value. These functions are SYNCHRONOUS, so a
2330
+ // library-style API can call them in a getter; the engine must be loaded
2331
+ // first (await ready(), or any other call).
2332
+ // ---------------------------------------------------------------
2333
+ function loadedEntry(who) {
2334
+ if (!ctxCache) {
2335
+ throw new FactoidalError(
2336
+ `${who}: the engine is not loaded yet. Await any @factoidal/core call ` +
2337
+ 'first (for example `await ready()`); after that this function is synchronous.',
2338
+ { code: 'op.unknown' });
2339
+ }
2340
+ return ctxCache.e;
2341
+ }
2342
+
2343
+ function literalAnswer(term, who) {
2344
+ if (!term || typeof term !== 'object' || term.termType !== 'Literal') {
2345
+ throw new TypeError(`${who}: expected an RDF/JS Literal term`);
2346
+ }
2347
+ const e = loadedEntry(who);
2348
+ requireEntryFn(e, 'literalValue', who);
2349
+ return entryResult(e.literalValue(JSON.stringify(termToJson(term))), who);
2350
+ }
2351
+
2352
+ // NaN and the infinities have no JSON number; the engine names them in
2353
+ // `special`, and this copies the matching JavaScript number in.
2354
+ const SPECIAL_NUMBERS = { NaN: NaN, INF: Infinity, '-INF': -Infinity };
2355
+ function valueOut(v) {
2356
+ if (v && typeof v.special === 'string' && v.special in SPECIAL_NUMBERS) {
2357
+ return { ...v, number: SPECIAL_NUMBERS[v.special] };
2358
+ }
2359
+ // A JSON text -0 may reach this layer as 0; negativeZero is the engine's
2360
+ // statement of the sign.
2361
+ if (v && v.negativeZero === true) return { ...v, number: -0 };
2362
+ return v;
2363
+ }
2364
+
2365
+ /**
2366
+ * The value of a literal: {datatype, recognized, valid, value?, canonical?}.
2367
+ * `recognized` false: the engine has no lexical space for the datatype, and
2368
+ * `valid` is null. `value` and `canonical` are present for a valid literal
2369
+ * of a datatype with a value model (the shapes are in API.md).
2370
+ */
2371
+ function literalValue(term) {
2372
+ const r = literalAnswer(term, 'literalValue');
2373
+ const out = { datatype: r.datatype, recognized: r.recognized,
2374
+ valid: r.recognized ? r.valid : null };
2375
+ if (r.value !== undefined) out.value = valueOut(r.value);
2376
+ if (r.canonical !== undefined) out.canonical = r.canonical;
2377
+ return out;
2378
+ }
2379
+
2380
+ /** true or false: the lexical form is in the datatype's lexical space;
2381
+ * null: the engine has no lexical space for the datatype. */
2382
+ function isValidLiteral(term) {
2383
+ const r = literalAnswer(term, 'isValidLiteral');
2384
+ return r.recognized ? r.valid : null;
2385
+ }
2386
+
2387
+ /** The literal with the canonical lexical form of its value (XSD 1.1 Part 2
2388
+ * section 2.3.1), the same datatype and language; null when the literal is
2389
+ * not valid or the engine has no canonical mapping for its datatype. */
2390
+ function canonicalLiteral(term) {
2391
+ const r = literalAnswer(term, 'canonicalLiteral');
2392
+ if (r.canonical === undefined) return null;
2393
+ if (term.language) return dataFactory.literal(r.canonical, term.direction
2394
+ ? { language: term.language, direction: term.direction } : term.language);
2395
+ return dataFactory.literal(r.canonical, dataFactory.namedNode(r.datatype));
2396
+ }
2397
+
2398
+ /**
2399
+ * The literal of a JavaScript value. The engine chooses the datatype (table
2400
+ * in API.md) unless `options.datatype` names one (an IRI, a NamedNode, or
2401
+ * an XSD built-in local name such as 'int'); the value must then be in that
2402
+ * datatype's value space, else a FactoidalError with code literal.value.
2403
+ */
2404
+ function literalFromValue(value, options) {
2405
+ const who = 'literalFromValue';
2406
+ const tagged = encodeJsValue(value);
2407
+ if (!tagged) {
2408
+ throw new TypeError(`${who}: expected a boolean, number, bigint, string or Date`);
2409
+ }
2410
+ const dt = options && options.datatype;
2411
+ const hint = dt === undefined || dt === null ? ''
2412
+ : (typeof dt === 'string' ? dt : (dt && typeof dt.value === 'string' ? dt.value : null));
2413
+ if (hint === null) throw new TypeError(`${who}: options.datatype must be a string or a NamedNode`);
2414
+ const e = loadedEntry(who);
2415
+ requireEntryFn(e, 'literalFromValue', who);
2416
+ const r = entryResult(e.literalFromValue(JSON.stringify(tagged), hint), who);
2417
+ return dataFactory.literal(r.value, dataFactory.namedNode(r.datatype));
2418
+ }
2419
+
2204
2420
  // The node-composition API (lib/nodes.mjs, issue 1002 step 2).
2205
2421
  const nodeApi = buildNodes({
2206
2422
  entry, entryResult, requireEntryFn, Dataset,
2207
- parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf,
2423
+ parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf,
2208
2424
  });
2209
2425
 
2210
2426
  return {
@@ -2224,6 +2440,7 @@ function buildApi(driver) {
2224
2440
  canonicalize,
2225
2441
  graphs,
2226
2442
  canonicalHash,
2443
+ asDefaultGraph,
2227
2444
  shaclValidate,
2228
2445
  shexValidate,
2229
2446
  owlClosure,
@@ -2287,8 +2504,19 @@ function buildApi(driver) {
2287
2504
  queryRaw,
2288
2505
  isomorphic,
2289
2506
  fromQuads,
2507
+ literalValue,
2508
+ isValidLiteral,
2509
+ canonicalLiteral,
2510
+ literalFromValue,
2290
2511
  ready,
2291
2512
  engineTables,
2513
+ sameValue,
2514
+ syntaxFor,
2515
+ splitIri,
2516
+ isNCName,
2517
+ vocabularies,
2518
+ memberIri,
2519
+ memberOrdinal,
2292
2520
  Dataset,
2293
2521
  Store,
2294
2522
  FactoidalError,
package/lib/dataset.mjs CHANGED
@@ -405,22 +405,44 @@ class Dataset {
405
405
  termToJson(graph)]);
406
406
  }
407
407
 
408
- /** RDF/JS DatasetCore.has: one indexed lookup in the engine. */
409
- has(quad) {
408
+ // The options argument of has, match and delete: `{match: 'term'|'value'}`,
409
+ // checked by the engine (op apiCheck). '' for the term match, which every
410
+ // engine serves with the two-argument ops.
411
+ _matchMode(who, options) {
412
+ if (options === undefined || options === null) return '';
413
+ checkOptions(this._ctx.e, `Dataset.${who}`, options);
414
+ if (options.match === undefined || options.match === null) return '';
415
+ const mode = String(options.match).toLowerCase();
416
+ return mode === 'term' ? '' : JSON.stringify({ match: mode });
417
+ }
418
+
419
+ /**
420
+ * RDF/JS DatasetCore.has: one indexed lookup in the engine. With
421
+ * `{match: 'value'}` a literal object matches every object literal of the
422
+ * same value (sameValue(); https://github.com/danbri/factoidal/issues/1052).
423
+ */
424
+ has(quad, options) {
410
425
  this._open('has');
411
- const r = envelope(this._ctx.e.datasetMatchCount(this._handle,
412
- this._pattern(quad.subject, quad.predicate, quad.object, quad.graph || { termType: 'DefaultGraph' })), 'has');
426
+ const mode = this._matchMode('has', options);
427
+ const pattern = this._pattern(quad.subject, quad.predicate, quad.object, quad.graph || { termType: 'DefaultGraph' });
428
+ const r = envelope(mode
429
+ ? this._ctx.e.datasetMatchCount(this._handle, pattern, mode)
430
+ : this._ctx.e.datasetMatchCount(this._handle, pattern), 'has');
413
431
  return r.count > 0;
414
432
  }
415
433
 
416
434
  /**
417
435
  * RDF/JS DatasetCore.match: the matching quads as a new Dataset, found
418
- * through the engine's indexes. `null`/`undefined` matches any term.
436
+ * through the engine's indexes. `null`/`undefined` matches any term. With
437
+ * `{match: 'value'}` a literal object matches by value (as has()).
419
438
  */
420
- match(subject, predicate, object, graph) {
439
+ match(subject, predicate, object, graph, options) {
421
440
  this._open('match');
422
- const r = envelope(this._ctx.e.datasetMatch(this._handle,
423
- this._pattern(subject, predicate, object, graph)), 'match');
441
+ const mode = this._matchMode('match', options);
442
+ const pattern = this._pattern(subject, predicate, object, graph);
443
+ const r = envelope(mode
444
+ ? this._ctx.e.datasetMatch(this._handle, pattern, mode)
445
+ : this._ctx.e.datasetMatch(this._handle, pattern), 'match');
424
446
  return this._ctx.wrap(r, this._prefixes);
425
447
  }
426
448
 
@@ -438,15 +460,77 @@ class Dataset {
438
460
  return this;
439
461
  }
440
462
 
441
- /** RDF/JS DatasetCore.delete: remove one quad. */
442
- delete(quad) {
463
+ /**
464
+ * RDF/JS DatasetCore.delete: remove one quad. With `{match: 'value'}` a
465
+ * literal object removes every quad of the same subject, predicate and
466
+ * graph whose object has the same value (as has()).
467
+ */
468
+ delete(quad, options) {
443
469
  this._open('delete');
444
- const r = envelope(this._ctx.e.datasetDeleteQuadsJson(this._handle,
445
- JSON.stringify([quadToJson(quad)])), 'delete');
470
+ const mode = this._matchMode('delete', options);
471
+ const quads = JSON.stringify([quadToJson(quad)]);
472
+ const r = envelope(mode
473
+ ? this._ctx.e.datasetDeleteQuadsJson(this._handle, quads, mode)
474
+ : this._ctx.e.datasetDeleteQuadsJson(this._handle, quads), 'delete');
446
475
  this._changed(r.count);
447
476
  return this;
448
477
  }
449
478
 
479
+ // RDF lists and containers (https://github.com/danbri/factoidal/issues/1051
480
+ // item 6). The engine walks and builds them (L4Factoidal.RDF.Collections).
481
+ // `graph`: null/undefined or the DefaultGraph for the default graph, else
482
+ // the graph name.
483
+ _collectionOp(op, who) {
484
+ this._open(who);
485
+ if (typeof this._ctx.e[op] !== 'function') {
486
+ throw new FactoidalError(`Dataset.${who}: the loaded engine has no op '${op}'; it is older than this package`,
487
+ { code: 'op.unknown' });
488
+ }
489
+ return this._ctx.e[op].bind(this._ctx.e);
490
+ }
491
+
492
+ _graphArg(graph) {
493
+ return graph === undefined || graph === null ? '' : JSON.stringify(termToJson(graph));
494
+ }
495
+
496
+ /**
497
+ * The items of the RDF list (rdf:first / rdf:rest) at `head`, in order.
498
+ * rdf:nil is the empty list. A list that is not well formed (a node with no
499
+ * or two rdf:first or rdf:rest, or a cycle) is a FactoidalError (op.failed)
500
+ * whose message names the reason and the node.
501
+ */
502
+ listItems(head, graph) {
503
+ const op = this._collectionOp('datasetListItems', 'listItems');
504
+ const r = envelope(op(this._handle, JSON.stringify(termToJson(head)), this._graphArg(graph)), 'listItems');
505
+ return r.items.map(termFromSrj);
506
+ }
507
+
508
+ /**
509
+ * The members rdf:_1, rdf:_2, ... of `node`, sorted by n, and its kind
510
+ * (rdf:Bag, rdf:Seq or rdf:Alt from its rdf:type, or null).
511
+ * @returns {{kind: NamedNode|null, items: Array<{ordinal: number, term: Term}>}}
512
+ */
513
+ containerItems(node, graph) {
514
+ const op = this._collectionOp('datasetContainerItems', 'containerItems');
515
+ const r = envelope(op(this._handle, JSON.stringify(termToJson(node)), this._graphArg(graph)), 'containerItems');
516
+ return {
517
+ kind: r.kind ? termFromSrj(r.kind) : null,
518
+ items: r.items.map((m) => ({ ordinal: m.ordinal, term: termFromSrj(m.term) })),
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Add a new RDF list of `items` (new blank nodes, fresh in this Dataset)
524
+ * and answer its head; the empty list is rdf:nil and adds nothing.
525
+ */
526
+ addList(items, graph) {
527
+ const op = this._collectionOp('datasetAddList', 'addList');
528
+ const r = envelope(op(this._handle, JSON.stringify([...items].map((t) => termToJson(t))),
529
+ this._graphArg(graph)), 'addList');
530
+ this._changed(r.count);
531
+ return termFromSrj(r.head);
532
+ }
533
+
450
534
  /** Release the engine copy. Idempotent. */
451
535
  dispose() {
452
536
  if (this._disposed) return;
@@ -63,22 +63,40 @@ function failEnvelope(err) {
63
63
  return JSON.stringify({ ok: false, code: 'op.failed', error: String((err && err.message) || err) });
64
64
  }
65
65
 
66
+ /**
67
+ * A JavaScript value tagged with its JavaScript type, {js, v}, for the engine
68
+ * (Wasm/Ops/ExtFns.lean, Wasm/Ops/LiteralValue.lean). This copies the value
69
+ * as text; the engine chooses the datatype and the lexical form
70
+ * (L4Factoidal.XSD.JsValue.jsLiteral, issue 894 item 3, issue 1051 item 1).
71
+ * A number is its Number::toString text ('-0' for negative zero); a Date is
72
+ * the text of getTime() ('NaN' for an invalid Date). Any other value: null.
73
+ * @param {unknown} value
74
+ * @returns {{js: string, v: string|boolean} | null}
75
+ */
76
+ export function encodeJsValue(value) {
77
+ switch (typeof value) {
78
+ case 'boolean': return { js: 'boolean', v: value };
79
+ case 'number': return { js: 'number', v: Object.is(value, -0) ? '-0' : String(value) };
80
+ case 'bigint': return { js: 'bigint', v: value.toString() };
81
+ case 'string': return { js: 'string', v: value };
82
+ case 'object':
83
+ if (value instanceof Date) return { js: 'date', v: String(value.getTime()) };
84
+ return null;
85
+ default: return null;
86
+ }
87
+ }
88
+
66
89
  // One answer for Wasm/Ops/ExtFns.lean: the JavaScript value tagged with its
67
- // JavaScript type ({js, v}; a number as its Number::toString text), or a term
68
- // object. The engine chooses the datatype and the lexical form
69
- // (L4Factoidal.XSD.JsValue.jsLiteral, issue 894 item 3). '' means "no
70
- // value" (the section 17.6 error).
90
+ // JavaScript type (encodeJsValue), or a term object. '' means "no value" (the
91
+ // section 17.6 error).
71
92
  function encodeExtAnswer(out) {
72
93
  if (out === null || out === undefined || out === EXT_PENDING_MARKER) return '';
73
- switch (typeof out) {
74
- case 'boolean': return JSON.stringify({ js: 'boolean', v: out });
75
- case 'number': return JSON.stringify({ js: 'number', v: Object.is(out, -0) ? '-0' : String(out) });
76
- case 'bigint': return JSON.stringify({ js: 'bigint', v: out.toString() });
77
- case 'string': return JSON.stringify({ js: 'string', v: out });
78
- case 'object':
79
- try { return JSON.stringify(appTermToJson(out)); } catch (_error) { return ''; }
80
- default: return '';
94
+ const tagged = encodeJsValue(out);
95
+ if (tagged) return JSON.stringify(tagged);
96
+ if (typeof out === 'object') {
97
+ try { return JSON.stringify(appTermToJson(out)); } catch (_error) { return ''; }
81
98
  }
99
+ return '';
82
100
  }
83
101
 
84
102
  // The Lean module asks its host through ONE global,