@factoidal/core 0.10.0 → 0.12.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/CHANGELOG.md +496 -1
- package/README.md +70 -4
- package/api.d.ts +2 -3
- package/api.mjs +2 -2
- package/bin/ext.mjs +16 -20
- package/bin/factoidal.mjs +308 -69
- package/browser.d.ts +7 -2
- package/browser.js +5 -2
- package/fn.d.ts +1 -1
- package/fn.js +33 -100
- package/index.d.ts +105 -29
- package/index.mjs +5 -2
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +214 -287
- package/lib/dataset.mjs +164 -75
- package/lib/delta-log-hosts.mjs +9 -8
- package/lib/errors.mjs +97 -0
- package/lib/lean-entry.mjs +36 -22
- package/lib/store.mjs +4 -5
- package/lib/version.mjs +1 -1
- package/lws/README.md +114 -7
- package/lws/persist.mjs +309 -0
- package/lws/server.mjs +364 -17
- package/package.json +2 -1
- package/rdfjs.mjs +151 -226
- package/solid/client/index.mjs +25 -9
- package/solid/server/index.mjs +99 -15
package/lib/dataset.mjs
CHANGED
|
@@ -13,49 +13,131 @@
|
|
|
13
13
|
// Lifetime: dispose() (or `using ds = ...`, Symbol.dispose) releases the
|
|
14
14
|
// engine copy. A Dataset that is garbage collected without dispose() is
|
|
15
15
|
// released by a FinalizationRegistry callback. Every method of a disposed
|
|
16
|
-
// Dataset throws
|
|
16
|
+
// Dataset throws the engine's FactoidalError, code handle.disposed.
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { dataFactory, quadFromJson, quadToJson, termToJson, termFromSrj, Literal, NamedNode } from '../rdfjs.mjs';
|
|
19
|
+
import { FactoidalError, envelopeResult } from './errors.mjs';
|
|
19
20
|
|
|
20
21
|
const INTERNAL = Symbol('factoidal.Dataset.internal');
|
|
21
22
|
|
|
22
|
-
// Writer names (lower case) -> the engine's writer tag
|
|
23
|
+
// Writer names (lower case) -> the engine's writer tag: the engine's table
|
|
24
|
+
// (op engineTables, `writerFormats`), read once per engine. JSON-LD is served
|
|
23
25
|
// by the op datasetToJsonLd; the SPARQL results formats are query outputs.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
turtle: 'turtle', ttl: 'turtle',
|
|
28
|
-
trig: 'trig',
|
|
29
|
-
rdfxml: 'rdfxml', 'rdf-xml': 'rdfxml', rdf: 'rdfxml',
|
|
30
|
-
jsonld: 'jsonld', 'json-ld': 'jsonld',
|
|
31
|
-
rdfc: 'rdfc', canonical: 'rdfc', 'rdfc-1.0': 'rdfc',
|
|
32
|
-
};
|
|
26
|
+
// `WRITER_TAGS` stays exported for callers that read it; it is filled from
|
|
27
|
+
// the engine on the first Dataset.
|
|
28
|
+
const WRITER_TAGS = {};
|
|
33
29
|
|
|
34
30
|
const VERSIONS = { '1.1': '11', '1.2': '12' };
|
|
35
31
|
|
|
36
|
-
/**
|
|
37
|
-
function
|
|
32
|
+
/**
|
|
33
|
+
* The options object of public function `who`, checked by the engine (op
|
|
34
|
+
* apiCheck, formal/lean4/Wasm/Ops/Handles.lean): every key one the function
|
|
35
|
+
* takes, every format, regime, output and version in the engine's tables, and
|
|
36
|
+
* no two options that cannot be combined. A value that is not an object is a
|
|
37
|
+
* TypeError (a wrong JavaScript type); every other refusal is the engine's
|
|
38
|
+
* FactoidalError.
|
|
39
|
+
*/
|
|
40
|
+
function checkOptions(e, who, options) {
|
|
38
41
|
if (options == null) return {};
|
|
39
42
|
if (typeof options !== 'object' || Array.isArray(options)) {
|
|
40
43
|
throw new TypeError(`${who}: options must be an object, got ${Array.isArray(options) ? 'array' : typeof options}`);
|
|
41
44
|
}
|
|
45
|
+
// Each value as the engine reads it: a primitive as its String form, an
|
|
46
|
+
// object, array or function as {}, undefined as null.
|
|
47
|
+
const wire = {};
|
|
42
48
|
for (const key of Object.keys(options)) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
49
|
+
const v = options[key];
|
|
50
|
+
wire[key] = v === undefined || v === null ? null
|
|
51
|
+
: (typeof v === 'object' || typeof v === 'function') ? {} : String(v);
|
|
47
52
|
}
|
|
53
|
+
// An engine with no apiCheck op (the F* entry of tests/engine-compare, a
|
|
54
|
+
// private comparison build) has no option tables: its ops refuse instead.
|
|
55
|
+
if (typeof e.apiCheck !== 'function') return options;
|
|
56
|
+
envelopeResult(e.apiCheck(who, JSON.stringify(wire)));
|
|
48
57
|
return options;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
function envelope(jsonText,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
function envelope(jsonText, _what, text) {
|
|
61
|
+
return envelopeResult(jsonText, text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The quads of the compact answer of datasetQuadsJson: each distinct term is
|
|
65
|
+
// built once and shared (TermJson.compactCertified).
|
|
66
|
+
function quadsFromCompact(terms, ixs) {
|
|
67
|
+
const ts = terms.map(termFromSrj);
|
|
68
|
+
const dg = dataFactory.defaultGraph();
|
|
69
|
+
return ixs.map((ix) => dataFactory.quad(ts[ix[0]], ts[ix[1]], ts[ix[2]],
|
|
70
|
+
ix.length > 3 ? ts[ix[3]] : dg));
|
|
55
71
|
}
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
// The region of op datasetQuadsBin, read as the engine wrote it
|
|
74
|
+
// (formal/lean4/L4Factoidal/RDF/QuadsBin.lean: `binSpec` is the layout,
|
|
75
|
+
// `decodeBin` the reader this function follows, `decodeBin_quadsBin` the
|
|
76
|
+
// theorem that the region reads back as exactly the quads). Every number is a
|
|
77
|
+
// u32 little-endian; each term is in the term codec of DLOG version 2, which
|
|
78
|
+
// names the term kind, the datatype IRI, the language tag and the base
|
|
79
|
+
// direction explicitly, so this function copies fields and decides nothing.
|
|
80
|
+
const UTF8 = new TextDecoder('utf-8', { fatal: true });
|
|
81
|
+
const DIRECTION = ['', 'ltr', 'rtl'];
|
|
82
|
+
|
|
83
|
+
function quadsFromRegion(bytes, out) {
|
|
84
|
+
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
85
|
+
if (bytes.length < 12 || v.getUint32(0, true) !== 0x31425146) {
|
|
86
|
+
throw new FactoidalError('iterate: the engine answered no datasetQuadsBin region', { code: 'op.failed' });
|
|
87
|
+
}
|
|
88
|
+
const nTerms = v.getUint32(4, true);
|
|
89
|
+
const nQuads = v.getUint32(8, true);
|
|
90
|
+
let p = 12;
|
|
91
|
+
// UTF-8 bytes to a string; a short run of bytes below 0x80 (one UTF-16
|
|
92
|
+
// unit each) is copied without a TextDecoder call.
|
|
93
|
+
const str = () => {
|
|
94
|
+
const n = v.getUint32(p, true);
|
|
95
|
+
const a = p + 4;
|
|
96
|
+
p = a + n;
|
|
97
|
+
if (n <= 64) {
|
|
98
|
+
let i = a;
|
|
99
|
+
while (i < p && bytes[i] < 0x80) i++;
|
|
100
|
+
if (i === p) return String.fromCharCode.apply(null, bytes.subarray(a, p));
|
|
101
|
+
}
|
|
102
|
+
return UTF8.decode(bytes.subarray(a, p));
|
|
103
|
+
};
|
|
104
|
+
const datatypes = new Map();
|
|
105
|
+
const term = () => {
|
|
106
|
+
switch (bytes[p++]) {
|
|
107
|
+
case 0: return new NamedNode(str());
|
|
108
|
+
case 1: return dataFactory.blankNode(str());
|
|
109
|
+
case 2: {
|
|
110
|
+
const lexical = str();
|
|
111
|
+
const dt = str();
|
|
112
|
+
let datatype = datatypes.get(dt);
|
|
113
|
+
if (datatype === undefined) { datatype = new NamedNode(dt); datatypes.set(dt, datatype); }
|
|
114
|
+
const flag = bytes[p++];
|
|
115
|
+
const language = flag === 0 ? '' : str();
|
|
116
|
+
const direction = flag === 2 ? DIRECTION[bytes[p++]] : '';
|
|
117
|
+
return new Literal(lexical, language, datatype, direction);
|
|
118
|
+
}
|
|
119
|
+
case 3: {
|
|
120
|
+
const s = term();
|
|
121
|
+
const pr = term();
|
|
122
|
+
return dataFactory.quad(s, pr, term());
|
|
123
|
+
}
|
|
124
|
+
default: throw new FactoidalError('iterate: a term tag outside the DLOG version 2 codec', { code: 'op.failed' });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const ts = new Array(nTerms);
|
|
128
|
+
for (let i = 0; i < nTerms; i++) ts[i] = term();
|
|
129
|
+
const dg = dataFactory.defaultGraph();
|
|
130
|
+
for (let i = 0; i < nQuads; i++, p += 16) {
|
|
131
|
+
const g = v.getUint32(p + 12, true);
|
|
132
|
+
out.push(dataFactory.quad(ts[v.getUint32(p, true)], ts[v.getUint32(p + 4, true)],
|
|
133
|
+
ts[v.getUint32(p + 8, true)], g === 0 ? dg : ts[g - 1]));
|
|
134
|
+
}
|
|
135
|
+
return nQuads;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Quads of the first datasetQuadsBin call while iterating: an iteration that
|
|
139
|
+
// stops early reads this many; the rest come in one more call.
|
|
140
|
+
const FIRST_PAGE = 4096;
|
|
59
141
|
|
|
60
142
|
class Dataset {
|
|
61
143
|
/** @private Datasets come from parse(), csvwToRdf() and the other producers. */
|
|
@@ -99,9 +181,11 @@ class Dataset {
|
|
|
99
181
|
*/
|
|
100
182
|
get info() { return this._info; }
|
|
101
183
|
|
|
184
|
+
// A disposed Dataset: the engine answers handle.disposed for its handle.
|
|
102
185
|
_open(who) {
|
|
103
186
|
if (this._disposed) {
|
|
104
|
-
|
|
187
|
+
envelopeResult(this._ctx.e.datasetClose(this._handle));
|
|
188
|
+
throw new FactoidalError(`Dataset.${who}: the Dataset was disposed`, { code: 'handle.disposed' });
|
|
105
189
|
}
|
|
106
190
|
}
|
|
107
191
|
|
|
@@ -123,7 +207,7 @@ class Dataset {
|
|
|
123
207
|
async query(sparql, options) {
|
|
124
208
|
this._open('query');
|
|
125
209
|
return this._ctx.queryHandle(this, sparql,
|
|
126
|
-
checkOptions('Dataset.query', options
|
|
210
|
+
checkOptions(this._ctx.e, 'Dataset.query', options));
|
|
127
211
|
}
|
|
128
212
|
|
|
129
213
|
/**
|
|
@@ -134,11 +218,11 @@ class Dataset {
|
|
|
134
218
|
*/
|
|
135
219
|
async update(updateText, options) {
|
|
136
220
|
this._open('update');
|
|
137
|
-
checkOptions('Dataset.update', options
|
|
221
|
+
checkOptions(this._ctx.e, 'Dataset.update', options);
|
|
138
222
|
if (typeof updateText !== 'string') {
|
|
139
223
|
throw new TypeError('Dataset.update: updateText must be a string');
|
|
140
224
|
}
|
|
141
|
-
const r = envelope(this._ctx.e.datasetUpdate(this._handle, updateText), 'update');
|
|
225
|
+
const r = envelope(this._ctx.e.datasetUpdate(this._handle, updateText), 'update', updateText);
|
|
142
226
|
this._changed(r.count);
|
|
143
227
|
return this;
|
|
144
228
|
}
|
|
@@ -152,7 +236,7 @@ class Dataset {
|
|
|
152
236
|
async validateShacl(shapes, options) {
|
|
153
237
|
this._open('validateShacl');
|
|
154
238
|
return this._ctx.shacl(this, shapes,
|
|
155
|
-
checkOptions('Dataset.validateShacl', options
|
|
239
|
+
checkOptions(this._ctx.e, 'Dataset.validateShacl', options));
|
|
156
240
|
}
|
|
157
241
|
|
|
158
242
|
/**
|
|
@@ -164,7 +248,7 @@ class Dataset {
|
|
|
164
248
|
async validateShex(schema, options) {
|
|
165
249
|
this._open('validateShex');
|
|
166
250
|
return this._ctx.shex(this, schema,
|
|
167
|
-
checkOptions('Dataset.validateShex', options
|
|
251
|
+
checkOptions(this._ctx.e, 'Dataset.validateShex', options));
|
|
168
252
|
}
|
|
169
253
|
|
|
170
254
|
/**
|
|
@@ -194,8 +278,8 @@ class Dataset {
|
|
|
194
278
|
*/
|
|
195
279
|
async serialize(options) {
|
|
196
280
|
this._open('serialize');
|
|
197
|
-
|
|
198
|
-
return this._write(checkOptions('Dataset.serialize',
|
|
281
|
+
const opts = typeof options === 'string' ? { format: options } : options;
|
|
282
|
+
return this._write(checkOptions(this._ctx.e, 'Dataset.serialize', opts));
|
|
199
283
|
}
|
|
200
284
|
|
|
201
285
|
/**
|
|
@@ -205,33 +289,18 @@ class Dataset {
|
|
|
205
289
|
*/
|
|
206
290
|
toString(formatOrOptions) {
|
|
207
291
|
this._open('toString');
|
|
208
|
-
const opts = typeof formatOrOptions === 'string'
|
|
209
|
-
|
|
210
|
-
: checkOptions('Dataset.toString', formatOrOptions, SERIALIZE_OPTIONS);
|
|
211
|
-
return this._write(opts);
|
|
292
|
+
const opts = typeof formatOrOptions === 'string' ? { format: formatOrOptions } : formatOrOptions;
|
|
293
|
+
return this._write(checkOptions(this._ctx.e, 'Dataset.toString', opts));
|
|
212
294
|
}
|
|
213
295
|
|
|
214
296
|
_write(opts) {
|
|
215
297
|
const e = this._ctx.e;
|
|
216
298
|
const name = String(opts.format || 'nquads').toLowerCase();
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
let suffix = '';
|
|
224
|
-
if (opts.version !== undefined) {
|
|
225
|
-
suffix = VERSIONS[String(opts.version)];
|
|
226
|
-
if (!suffix) throw new TypeError("Dataset.serialize: version must be '1.1' or '1.2'");
|
|
227
|
-
}
|
|
228
|
-
if (tag !== 'turtle' && (opts.prefixes !== undefined || opts.literalShorthand !== undefined)) {
|
|
229
|
-
throw new TypeError("Dataset.serialize: 'prefixes' and 'literalShorthand' apply to format 'turtle' only");
|
|
230
|
-
}
|
|
231
|
-
const jsonldKeys = ['useNativeTypes', 'useRdfType', 'rdfDirection'];
|
|
232
|
-
if (tag !== 'jsonld' && jsonldKeys.some((k) => opts[k] !== undefined)) {
|
|
233
|
-
throw new TypeError(`Dataset.serialize: ${jsonldKeys.join(', ')} apply to format 'jsonld' only`);
|
|
234
|
-
}
|
|
299
|
+
const writerTags = this._ctx.tables().writerFormats;
|
|
300
|
+
// The engine checked the format, the version and the option
|
|
301
|
+
// combination (op apiCheck) before this.
|
|
302
|
+
const tag = writerTags[name];
|
|
303
|
+
const suffix = opts.version !== undefined ? VERSIONS[String(opts.version)] : '';
|
|
235
304
|
if (tag === 'jsonld') {
|
|
236
305
|
if (typeof e.datasetToJsonLd !== 'function') throw this._ctx.missing('serialize {format:jsonld}', 'datasetToJsonLd');
|
|
237
306
|
const r = envelope(e.datasetToJsonLd(this._handle, JSON.stringify({
|
|
@@ -273,40 +342,58 @@ class Dataset {
|
|
|
273
342
|
}
|
|
274
343
|
|
|
275
344
|
/**
|
|
276
|
-
* RDF/JS quads,
|
|
345
|
+
* RDF/JS quads, built from the engine's term objects (op datasetQuadsJson),
|
|
346
|
+
* in the line order of toNQuads(). No N-Quads text is read here.
|
|
277
347
|
* @returns {Iterator<object>}
|
|
278
348
|
*/
|
|
279
349
|
*[Symbol.iterator]() {
|
|
280
350
|
this._open('iterate');
|
|
281
351
|
if (this._quads) { yield* this._quads; return; }
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
352
|
+
const e = this._ctx.e;
|
|
353
|
+
if (typeof e.datasetQuadsBin !== 'function') { yield* this._allQuads(); return; }
|
|
354
|
+
// The engine's region (op datasetQuadsBin) in two calls, a first page and
|
|
355
|
+
// the rest; the whole list is cached when the rest is read.
|
|
356
|
+
const acc = [];
|
|
357
|
+
const first = e.datasetQuadsBin(this._handle, `{"offset":0,"limit":${FIRST_PAGE}}`);
|
|
358
|
+
quadsFromRegion(first.bytes, acc);
|
|
359
|
+
for (let i = 0; i < acc.length; i++) yield acc[i];
|
|
360
|
+
if (acc.length < first.envelope.total) {
|
|
361
|
+
const rest = e.datasetQuadsBin(this._handle, `{"offset":${acc.length}}`);
|
|
362
|
+
const from = acc.length;
|
|
363
|
+
quadsFromRegion(rest.bytes, acc);
|
|
364
|
+
for (let i = from; i < acc.length; i++) yield acc[i];
|
|
365
|
+
}
|
|
366
|
+
if (!this._quads) this._quads = Object.freeze(acc);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Every quad, read once and cached.
|
|
370
|
+
_allQuads() {
|
|
371
|
+
if (!this._quads) {
|
|
372
|
+
const e = this._ctx.e;
|
|
373
|
+
if (typeof e.datasetQuadsBin === 'function') {
|
|
374
|
+
const acc = [];
|
|
375
|
+
quadsFromRegion(e.datasetQuadsBin(this._handle, '').bytes, acc);
|
|
376
|
+
this._quads = Object.freeze(acc);
|
|
377
|
+
} else {
|
|
378
|
+
const r = envelope(e.datasetQuadsJson(this._handle, '{"compact":true}'), 'iterate');
|
|
379
|
+
this._quads = Object.freeze(r.ixs ? quadsFromCompact(r.terms, r.ixs) : r.quads.map((q) => quadFromJson(q)));
|
|
380
|
+
}
|
|
293
381
|
}
|
|
382
|
+
return this._quads;
|
|
294
383
|
}
|
|
295
384
|
|
|
296
385
|
/** Every quad as an array. */
|
|
297
386
|
toArray() {
|
|
298
387
|
this._open('toArray');
|
|
299
|
-
|
|
300
|
-
return this._quads.slice();
|
|
388
|
+
return this._allQuads().slice();
|
|
301
389
|
}
|
|
302
390
|
|
|
303
391
|
// A quad pattern for the ops datasetMatch and datasetMatchCount: four
|
|
304
|
-
//
|
|
305
|
-
// default graph.
|
|
392
|
+
// engine term objects, null for an unbound position, {type:'default'} for
|
|
393
|
+
// the default graph.
|
|
306
394
|
_pattern(subject, predicate, object, graph) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return JSON.stringify([tok(subject), tok(predicate), tok(object), g]);
|
|
395
|
+
return JSON.stringify([termToJson(subject), termToJson(predicate), termToJson(object),
|
|
396
|
+
termToJson(graph)]);
|
|
310
397
|
}
|
|
311
398
|
|
|
312
399
|
/** RDF/JS DatasetCore.has: one indexed lookup in the engine. */
|
|
@@ -336,7 +423,8 @@ class Dataset {
|
|
|
336
423
|
*/
|
|
337
424
|
add(quad) {
|
|
338
425
|
this._open('add');
|
|
339
|
-
const r = envelope(this._ctx.e.
|
|
426
|
+
const r = envelope(this._ctx.e.datasetAddQuadsJson(this._handle,
|
|
427
|
+
JSON.stringify([quadToJson(quad)])), 'add');
|
|
340
428
|
this._changed(r.count);
|
|
341
429
|
return this;
|
|
342
430
|
}
|
|
@@ -344,7 +432,8 @@ class Dataset {
|
|
|
344
432
|
/** RDF/JS DatasetCore.delete: remove one quad. */
|
|
345
433
|
delete(quad) {
|
|
346
434
|
this._open('delete');
|
|
347
|
-
const r = envelope(this._ctx.e.
|
|
435
|
+
const r = envelope(this._ctx.e.datasetDeleteQuadsJson(this._handle,
|
|
436
|
+
JSON.stringify([quadToJson(quad)])), 'delete');
|
|
348
437
|
this._changed(r.count);
|
|
349
438
|
return this;
|
|
350
439
|
}
|
package/lib/delta-log-hosts.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// would; false when the log is empty
|
|
21
21
|
//
|
|
22
22
|
// No Node built-ins: the browser imports this file as it is.
|
|
23
|
+
import { hostError } from './errors.mjs';
|
|
23
24
|
|
|
24
25
|
const DELTA_STORE = 'deltaBatches';
|
|
25
26
|
|
|
@@ -65,7 +66,7 @@ export function memoryDeltaLogStore() {
|
|
|
65
66
|
function indexedDbFactory() {
|
|
66
67
|
const factory = globalThis.indexedDB;
|
|
67
68
|
if (!factory) {
|
|
68
|
-
throw
|
|
69
|
+
throw hostError('deltaLog: this runtime has no indexedDB; the browser entry keeps the log in IndexedDB');
|
|
69
70
|
}
|
|
70
71
|
return factory;
|
|
71
72
|
}
|
|
@@ -80,23 +81,23 @@ function idbOpen(dbName) {
|
|
|
80
81
|
}
|
|
81
82
|
};
|
|
82
83
|
req.onsuccess = () => resolve(req.result);
|
|
83
|
-
req.onerror = () => reject(
|
|
84
|
-
req.onblocked = () => reject(
|
|
84
|
+
req.onerror = () => reject(hostError('indexedDB.open failed', req.error));
|
|
85
|
+
req.onblocked = () => reject(hostError('indexedDB.open blocked (another tab holds an open connection at an older version)'));
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
function requestDone(req) {
|
|
89
90
|
return new Promise((resolve, reject) => {
|
|
90
91
|
req.onsuccess = () => resolve(req.result);
|
|
91
|
-
req.onerror = () => reject(
|
|
92
|
+
req.onerror = () => reject(hostError('IndexedDB request failed', req.error));
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
function transactionDone(tx) {
|
|
96
97
|
return new Promise((resolve, reject) => {
|
|
97
98
|
tx.oncomplete = () => resolve();
|
|
98
|
-
tx.onerror = () => reject(
|
|
99
|
-
tx.onabort = () => reject(
|
|
99
|
+
tx.onerror = () => reject(hostError('IndexedDB transaction failed', tx.error));
|
|
100
|
+
tx.onabort = () => reject(hostError('IndexedDB transaction aborted', tx.error));
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -164,8 +165,8 @@ export function indexedDbDeltaLogStore() {
|
|
|
164
165
|
await new Promise((resolve, reject) => {
|
|
165
166
|
const req = indexedDbFactory().deleteDatabase(name);
|
|
166
167
|
req.onsuccess = () => resolve();
|
|
167
|
-
req.onerror = () => reject(
|
|
168
|
-
req.onblocked = () => reject(
|
|
168
|
+
req.onerror = () => reject(hostError('indexedDB.deleteDatabase failed', req.error));
|
|
169
|
+
req.onblocked = () => reject(hostError('indexedDB.deleteDatabase blocked (another open connection)'));
|
|
169
170
|
});
|
|
170
171
|
},
|
|
171
172
|
async corruptLast(name) {
|
package/lib/errors.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// factoidal — the one error class of the package
|
|
2
|
+
// (https://github.com/danbri/factoidal/issues/882 item 4).
|
|
3
|
+
//
|
|
4
|
+
// Every function of the package rejects (or throws) with a FactoidalError.
|
|
5
|
+
// The engine decides its `code`, its `spec` section and its `position`
|
|
6
|
+
// (formal/lean4/Wasm/Ops/ErrorModel.lean); this file only copies them out
|
|
7
|
+
// of the error envelope. A TypeError is thrown only when an argument has the
|
|
8
|
+
// wrong JavaScript type (a number where a string is needed, and the like).
|
|
9
|
+
//
|
|
10
|
+
// ES module, no Node built-ins: the browser loads this file as it is.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An engine refusal, or a host failure (`host.io`).
|
|
14
|
+
*
|
|
15
|
+
* - `code`: a stable machine code, one of the engine's `errorCodes` table
|
|
16
|
+
* (op engineTables; README "Errors").
|
|
17
|
+
* - `position`: `{line, column, offset, byteOffset}` where the input defines
|
|
18
|
+
* one, else `null`. `line` and `column` are 1-based (`column` counts
|
|
19
|
+
* Unicode scalar values); `offset` is a UTF-16 index into the JavaScript
|
|
20
|
+
* string, so `text.slice(err.position.offset)` starts at the error;
|
|
21
|
+
* `byteOffset` counts UTF-8 bytes.
|
|
22
|
+
* - `spec`: the URL of the specification section that defines the
|
|
23
|
+
* condition, or `null`.
|
|
24
|
+
* - `cause`: the underlying error, when there is one.
|
|
25
|
+
* - `format`, `formatInferred`: for a parse failure of text, the syntax it
|
|
26
|
+
* was read as and whether that syntax was inferred.
|
|
27
|
+
*/
|
|
28
|
+
export class FactoidalError extends Error {
|
|
29
|
+
constructor(message, info) {
|
|
30
|
+
const i = info || {};
|
|
31
|
+
super(message, i.cause !== undefined ? { cause: i.cause } : undefined);
|
|
32
|
+
this.name = 'FactoidalError';
|
|
33
|
+
this.code = i.code || 'op.failed';
|
|
34
|
+
this.position = i.position || null;
|
|
35
|
+
this.spec = i.spec || null;
|
|
36
|
+
if (i.format !== undefined) this.format = i.format;
|
|
37
|
+
if (i.formatInferred !== undefined) this.formatInferred = i.formatInferred;
|
|
38
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
39
|
+
Error.captureStackTrace(this, FactoidalError);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The engine counts `offset` in Unicode scalar values from the start of the
|
|
45
|
+
// input; a JavaScript string is indexed in UTF-16 code units, where a
|
|
46
|
+
// character at U+10000 or above takes two. This converts the count to the
|
|
47
|
+
// UTF-16 index of the same position (a unit conversion of the host string;
|
|
48
|
+
// a count past the end gives text.length).
|
|
49
|
+
export function utf16OffsetOf(text, scalarOffset) {
|
|
50
|
+
if (typeof text !== 'string' || !Number.isInteger(scalarOffset) || scalarOffset < 0) {
|
|
51
|
+
return scalarOffset;
|
|
52
|
+
}
|
|
53
|
+
let units = 0;
|
|
54
|
+
let scalars = 0;
|
|
55
|
+
for (const ch of text) {
|
|
56
|
+
if (scalars >= scalarOffset) break;
|
|
57
|
+
units += ch.length;
|
|
58
|
+
scalars += 1;
|
|
59
|
+
}
|
|
60
|
+
return units;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The FactoidalError of an error envelope ({"ok":false,"error",...}).
|
|
65
|
+
* `text` is the input string the envelope's position counts into, when the
|
|
66
|
+
* caller has it.
|
|
67
|
+
*/
|
|
68
|
+
export function errorFromEnvelope(env, text) {
|
|
69
|
+
const p = env && env.position;
|
|
70
|
+
let position = null;
|
|
71
|
+
if (p && typeof p === 'object') {
|
|
72
|
+
position = {
|
|
73
|
+
line: p.line,
|
|
74
|
+
column: p.column,
|
|
75
|
+
offset: typeof text === 'string' ? utf16OffsetOf(text, p.offset) : p.offset,
|
|
76
|
+
byteOffset: p.byteOffset,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return new FactoidalError(String((env && env.error) || 'engine refusal'), {
|
|
80
|
+
code: env && env.code,
|
|
81
|
+
spec: env && env.spec,
|
|
82
|
+
position,
|
|
83
|
+
format: env && env.format,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Unwrap one op envelope: the envelope object, or throw its FactoidalError. */
|
|
88
|
+
export function envelopeResult(jsonText, text) {
|
|
89
|
+
const r = typeof jsonText === 'string' ? JSON.parse(jsonText) : jsonText;
|
|
90
|
+
if (!r.ok) throw errorFromEnvelope(r, text);
|
|
91
|
+
return r;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A host failure (file system, IndexedDB): code `host.io`, with its cause. */
|
|
95
|
+
export function hostError(message, cause) {
|
|
96
|
+
return new FactoidalError(message, { code: 'host.io', cause });
|
|
97
|
+
}
|
package/lib/lean-entry.mjs
CHANGED
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
// None of them makes a semantic decision: which IRIs reach the host, what
|
|
23
23
|
// an absent answer means, which artifact a query reads, are decided in Lean.
|
|
24
24
|
|
|
25
|
-
const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
|
26
25
|
|
|
27
26
|
// The two npm entries and the engine each loads (issue 713 stage 4). The
|
|
28
27
|
// core engine's `ops` reflection lists what only the full engine serves
|
|
29
28
|
// (`fullOnlyOps`, `fullOnlyFormats`); a call to one of those names the
|
|
30
29
|
// entry that has it.
|
|
30
|
+
import { appTermToJson, termToJson } from '../rdfjs.mjs';
|
|
31
|
+
import { errorFromEnvelope } from './errors.mjs';
|
|
31
32
|
export const FULL_ENTRY = '@factoidal/core';
|
|
32
33
|
// The core profile is built for the hub pages only; the package ships the
|
|
33
34
|
// full engine (owner, 2026-10-20).
|
|
@@ -53,33 +54,31 @@ function okEnvelope(extra) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// An engine refusal arrives as an Error whose `.envelope` is the whole
|
|
56
|
-
// {"ok":false,...} document
|
|
57
|
-
//
|
|
58
|
-
//
|
|
57
|
+
// {"ok":false,...} document: the engine's message, code, spec section and
|
|
58
|
+
// position pass to lib/errors.mjs unchanged. Any other throw (the wasm
|
|
59
|
+
// trapped, the loader failed) is code op.failed.
|
|
59
60
|
function failEnvelope(err) {
|
|
60
61
|
const env = err && err.envelope;
|
|
61
|
-
if (env && typeof env === 'object' && env.ok === false)
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
return JSON.stringify({ ok: false, error: String((err && err.message) || err) });
|
|
62
|
+
if (env && typeof env === 'object' && env.ok === false) return JSON.stringify(env);
|
|
63
|
+
return JSON.stringify({ ok: false, code: 'op.failed', error: String((err && err.message) || err) });
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
// One
|
|
68
|
-
//
|
|
66
|
+
// 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).
|
|
69
71
|
function encodeExtAnswer(out) {
|
|
70
72
|
if (out === null || out === undefined || out === EXT_PENDING_MARKER) return '';
|
|
71
|
-
|
|
72
|
-
return JSON.stringify({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 '';
|
|
79
81
|
}
|
|
80
|
-
if (typeof out === 'string') return JSON.stringify({ type: 'literal', value: out });
|
|
81
|
-
if (typeof out === 'object' && typeof out.type === 'string') return JSON.stringify(out);
|
|
82
|
-
return '';
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
// The Lean module asks its host through ONE global,
|
|
@@ -158,7 +157,7 @@ export function entryFromEngine(eng) {
|
|
|
158
157
|
entry[op] = (...args) => {
|
|
159
158
|
if (formatChecked && entry.fullOnlyFormats.includes(String(args[1] ?? '').toLowerCase())) {
|
|
160
159
|
return JSON.stringify({
|
|
161
|
-
ok: false, servedBy: 'full',
|
|
160
|
+
ok: false, servedBy: 'full', code: 'op.unknown',
|
|
162
161
|
error: fullEngineMessage(`the format '${args[1]}'`),
|
|
163
162
|
});
|
|
164
163
|
}
|
|
@@ -200,5 +199,20 @@ export function entryFromEngine(eng) {
|
|
|
200
199
|
}
|
|
201
200
|
};
|
|
202
201
|
}
|
|
202
|
+
// datasetQuadsBin answers a byte region (formal/lean4/L4Factoidal/RDF/QuadsBin.lean):
|
|
203
|
+
// this method answers {envelope, bytes} and throws on a refusal.
|
|
204
|
+
const blobIoOps = Array.isArray(reflection.blobIoOps) ? reflection.blobIoOps : [];
|
|
205
|
+
if (typeof eng.callBlobIO === 'function' && blobIoOps.includes('datasetQuadsBin')) {
|
|
206
|
+
entry.datasetQuadsBin = (handle, optsJson) => {
|
|
207
|
+
try {
|
|
208
|
+
return eng.callBlobIO('datasetQuadsBin', [String(handle), String(optsJson ?? '')], new Uint8Array(0));
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (err && err.envelope) throw errorFromEnvelope(err.envelope);
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
delete entry.datasetQuadsBin;
|
|
216
|
+
}
|
|
203
217
|
return entry;
|
|
204
218
|
}
|
package/lib/store.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// `factoidal` command.
|
|
18
18
|
|
|
19
19
|
import { requireChildName } from '../store-host/paths.mjs';
|
|
20
|
+
import { hostError } from './errors.mjs';
|
|
20
21
|
|
|
21
22
|
/** The manifest file names a generation directory can carry, in the order
|
|
22
23
|
* the native tools try them. */
|
|
@@ -44,10 +45,8 @@ export async function openGeneration(reader, generation) {
|
|
|
44
45
|
try {
|
|
45
46
|
pointer = await readBytes(reader, 'CURRENT');
|
|
46
47
|
} catch (cause) {
|
|
47
|
-
|
|
48
|
-
`openStore: the collection root has no readable CURRENT pointer (${cause && cause.message})
|
|
49
|
-
err.cause = cause;
|
|
50
|
-
throw err;
|
|
48
|
+
throw hostError(
|
|
49
|
+
`openStore: the collection root has no readable CURRENT pointer (${cause && cause.message})`, cause);
|
|
51
50
|
}
|
|
52
51
|
// CURRENT holds a UTF-8 generation name (docs/shardborough-storage-spec.md
|
|
53
52
|
// section 6.4); trailing ASCII whitespace is tolerated, as in
|
|
@@ -67,7 +66,7 @@ export async function openGeneration(reader, generation) {
|
|
|
67
66
|
}
|
|
68
67
|
return { generation: name, manifestName, manifest, activated: generation == null };
|
|
69
68
|
}
|
|
70
|
-
throw
|
|
69
|
+
throw hostError(`openStore: generation '${name}' has none of ${MANIFEST_NAMES.join(', ')}`);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
/** Read the named artifacts of one generation into one region, with a
|
package/lib/version.mjs
CHANGED