@factoidal/core 0.14.0 → 0.16.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 +24 -0
- package/README.md +4 -0
- package/browser.d.ts +6 -1
- package/browser.js +15 -2
- package/index.d.ts +140 -6
- package/index.mjs +24 -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 +12 -2
- package/lib/evidence.mjs +29 -5
- package/lib/nodes.mjs +141 -16
- package/lib/sbm11-lws.mjs +200 -0
- package/lib/sbm11.mjs +73 -1
- package/lib/sync-http-node.mjs +71 -0
- package/lib/version.mjs +1 -1
- package/package.json +1 -1
package/lib/nodes.mjs
CHANGED
|
@@ -14,15 +14,34 @@
|
|
|
14
14
|
// on a wire is `{ graph, evidence }`, and each node appends one evidence
|
|
15
15
|
// record `{ stage, status, contract, assumptions, checked, artefact }` (plus
|
|
16
16
|
// `unchecked` and `reason` for a step run under run()'s `unchecked` opt-in;
|
|
17
|
-
// the record logic is in evidence.mjs).
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// the record logic is in evidence.mjs).
|
|
18
|
+
//
|
|
19
|
+
// The type check and the assumption union are the engine's (op
|
|
20
|
+
// pipelineCheck, formal/lean4/Wasm/Ops/StageList.lean, computed by
|
|
21
|
+
// pipelineReport of formal/lean4/L4Factoidal/Dataflow/Pipeline.lean): a
|
|
22
|
+
// pipeline is well typed when each stage's output wire is the next stage's
|
|
23
|
+
// input wire or converts to it, and its `assumptions` are the premises of
|
|
24
|
+
// Pipeline.guarantee_sound, the guarantee of any pipeline of components with
|
|
25
|
+
// these signatures. This file does not compute the union. With an engine
|
|
26
|
+
// built before the op, pipe() and run() fall back to the union of the
|
|
27
|
+
// nodes' fields and report `wellTyped: null`.
|
|
28
|
+
//
|
|
29
|
+
// Node settings (https://github.com/danbri/factoidal/issues/975,
|
|
30
|
+
// https://github.com/danbri/factoidal/issues/1026): the params of node()
|
|
31
|
+
// are the node's settings. When the engine has the op pipelineCheckNodes
|
|
32
|
+
// (formal/lean4/Wasm/Ops/PipelineNodes.lean), the engine decides, per node:
|
|
33
|
+
// the settings in canonical form with their SHA-256 digest, the role of each
|
|
34
|
+
// setting in the stage's contract, the settings outside the proved domain
|
|
35
|
+
// (a proved stage with one is `unproved`), and the output wire the settings
|
|
36
|
+
// select (SELECT gives a Table, ASK an Answer, CONSTRUCT and DESCRIBE a
|
|
37
|
+
// Dataset, so a CONSTRUCT node can feed the next node). This file only marshals: a Dataset setting is sent as
|
|
38
|
+
// {"$nquads": its N-Quads text}. With an older engine, records carry no
|
|
39
|
+
// settings and the evaluator's output wire is the registry's.
|
|
21
40
|
//
|
|
22
41
|
// ES module, no Node built-ins: the browser loads this file as it is.
|
|
23
42
|
|
|
24
43
|
import { FactoidalError } from './errors.mjs';
|
|
25
|
-
import { refusalFor, makeRecord, summarize, unionAssumptions } from './evidence.mjs';
|
|
44
|
+
import { refusalFor, makeRecord, summarize, unionAssumptions, unionLists } from './evidence.mjs';
|
|
26
45
|
|
|
27
46
|
/**
|
|
28
47
|
* @param {object} deps the package functions and the engine entry
|
|
@@ -30,7 +49,7 @@ import { refusalFor, makeRecord, summarize, unionAssumptions } from './evidence.
|
|
|
30
49
|
*/
|
|
31
50
|
export function buildNodes(deps) {
|
|
32
51
|
const { entry, entryResult, requireEntryFn, Dataset,
|
|
33
|
-
parse, closure, shaclValidate, query, serialize, canonicalize, explain } = deps;
|
|
52
|
+
parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf } = deps;
|
|
34
53
|
|
|
35
54
|
const reader = (format) => async (wire, params) => {
|
|
36
55
|
if (typeof wire.graph !== 'string') {
|
|
@@ -81,6 +100,14 @@ export function buildNodes(deps) {
|
|
|
81
100
|
rdfcStage: async (wire, params) => ({
|
|
82
101
|
artefact: { canonical: await canonicalize(wire.graph, params) },
|
|
83
102
|
}),
|
|
103
|
+
'CSVW (tabular data to RDF)': async (wire, params) => {
|
|
104
|
+
if (typeof wire.graph !== 'string') {
|
|
105
|
+
throw new TypeError(`node CSVW: the stage takes CSV text on its wire (got ${typeof wire.graph})`);
|
|
106
|
+
}
|
|
107
|
+
const { metadata, ...opts } = params;
|
|
108
|
+
const meta = metadata === undefined || typeof metadata === 'string' ? metadata : JSON.stringify(metadata);
|
|
109
|
+
return { graph: await csvwToRdf(wire.graph, meta, opts), artefact: { tabular: 'csv' } };
|
|
110
|
+
},
|
|
84
111
|
rdfsExplainGraphStage: async (wire, params) => {
|
|
85
112
|
const { description, ...opts } = params;
|
|
86
113
|
if (description === undefined) {
|
|
@@ -91,6 +118,7 @@ export function buildNodes(deps) {
|
|
|
91
118
|
};
|
|
92
119
|
|
|
93
120
|
let stageCache = null;
|
|
121
|
+
let engine = null; // the entry stages() loaded; node() calls stages(), so pipe() has it
|
|
94
122
|
|
|
95
123
|
/**
|
|
96
124
|
* The engine's stage list (op stageList), one element per registry entry.
|
|
@@ -101,13 +129,17 @@ export function buildNodes(deps) {
|
|
|
101
129
|
stageCache = (async () => {
|
|
102
130
|
const e = await entry();
|
|
103
131
|
requireEntryFn(e, 'stageList', 'stages');
|
|
132
|
+
engine = e;
|
|
104
133
|
const r = entryResult(e.stageList(), 'stages');
|
|
105
134
|
return Object.freeze(r.stages.map((s) => Object.freeze({
|
|
106
135
|
...s,
|
|
107
136
|
theorems: Object.freeze([...s.theorems]),
|
|
108
137
|
assumptions: Object.freeze([...s.assumptions]),
|
|
109
138
|
tests: Object.freeze([...s.tests]),
|
|
110
|
-
bound:
|
|
139
|
+
// bound: the package has a runner AND the engine lets the stage
|
|
140
|
+
// run. A runner for a stage that an older engine reports as
|
|
141
|
+
// `missing` is not bound (the engine's registry decides).
|
|
142
|
+
bound: s.runnable && Object.prototype.hasOwnProperty.call(RUNNERS, s.name),
|
|
111
143
|
})));
|
|
112
144
|
})();
|
|
113
145
|
stageCache.catch(() => { stageCache = null; });
|
|
@@ -117,6 +149,32 @@ export function buildNodes(deps) {
|
|
|
117
149
|
|
|
118
150
|
const NODE = Symbol('factoidal.node');
|
|
119
151
|
|
|
152
|
+
// The settings as the engine reads them: JSON values, a Dataset as
|
|
153
|
+
// {"$nquads": text}. Anything else is refused.
|
|
154
|
+
function marshal(v, path) {
|
|
155
|
+
if (v === null || typeof v === 'string' || typeof v === 'boolean') return v;
|
|
156
|
+
if (typeof v === 'number') {
|
|
157
|
+
if (!Number.isFinite(v)) throw new TypeError(`node: setting ${path} is not a finite number`);
|
|
158
|
+
return v;
|
|
159
|
+
}
|
|
160
|
+
if (v instanceof Dataset) return { $nquads: v.toNQuads() };
|
|
161
|
+
if (Array.isArray(v)) return v.map((x, i) => marshal(x, `${path}[${i}]`));
|
|
162
|
+
if (typeof v === 'object' && Object.getPrototypeOf(v) === Object.prototype) {
|
|
163
|
+
const out = {};
|
|
164
|
+
for (const [k, x] of Object.entries(v)) if (x !== undefined) out[k] = marshal(x, `${path}.${k}`);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
throw new TypeError(`node: setting ${path} must be a JSON value or a Dataset (got ${typeof v})`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const hasNodeCheck = () => !!engine && typeof engine.pipelineCheckNodes === 'function';
|
|
171
|
+
|
|
172
|
+
// The engine's check of nodes with settings (op pipelineCheckNodes).
|
|
173
|
+
function nodeCheck(items) {
|
|
174
|
+
return entryResult(engine.pipelineCheckNodes(JSON.stringify(
|
|
175
|
+
items.map((n) => ({ stage: n.stage, settings: n.settingsWire })))), 'pipelineCheckNodes');
|
|
176
|
+
}
|
|
177
|
+
|
|
120
178
|
/**
|
|
121
179
|
* A node for the registered stage `name`, with its parameters (the
|
|
122
180
|
* second argument of the package function it calls, e.g. `{shapes}`,
|
|
@@ -135,18 +193,28 @@ export function buildNodes(deps) {
|
|
|
135
193
|
throw new FactoidalError(`node: no registered stage named '${name}' (see stages())`,
|
|
136
194
|
{ code: 'option.invalid-value' });
|
|
137
195
|
}
|
|
196
|
+
const settingsWire = marshal({ ...(params || {}) }, 'params');
|
|
197
|
+
// The engine's reading of the settings, when it has the op.
|
|
198
|
+
let s = null;
|
|
199
|
+
if (hasNodeCheck()) s = nodeCheck([{ stage: info.name, settingsWire }]).stages[0];
|
|
138
200
|
return Object.freeze({
|
|
139
201
|
[NODE]: true,
|
|
140
202
|
stage: info.name,
|
|
141
203
|
kind: info.kind,
|
|
142
|
-
status: info.status,
|
|
204
|
+
status: s ? s.status : info.status,
|
|
205
|
+
registryStatus: info.status,
|
|
143
206
|
contract: info.contract,
|
|
144
207
|
assumptions: info.assumptions,
|
|
145
208
|
wireIn: info.wireIn,
|
|
146
|
-
wireOut: info.wireOut,
|
|
209
|
+
wireOut: s ? s.wireOut : info.wireOut,
|
|
147
210
|
runnable: info.runnable,
|
|
148
211
|
bound: info.bound,
|
|
149
212
|
params: Object.freeze({ ...(params || {}) }),
|
|
213
|
+
settingsWire,
|
|
214
|
+
settings: s ? s.settings : null,
|
|
215
|
+
settingsDigest: s ? s.settingsDigest : null,
|
|
216
|
+
outside: Object.freeze(s ? s.outside.map((o) => Object.freeze({ ...o })) : []),
|
|
217
|
+
derived: s ? Object.freeze({ ...s.derived }) : null,
|
|
150
218
|
});
|
|
151
219
|
}
|
|
152
220
|
|
|
@@ -161,14 +229,49 @@ export function buildNodes(deps) {
|
|
|
161
229
|
}
|
|
162
230
|
|
|
163
231
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
232
|
+
* The engine's check of the stages of `nodes` in order (op pipelineCheck),
|
|
233
|
+
* or null when the loaded engine has no such op.
|
|
234
|
+
*/
|
|
235
|
+
function check(nodes) {
|
|
236
|
+
let r;
|
|
237
|
+
if (hasNodeCheck()) r = nodeCheck(nodes);
|
|
238
|
+
else if (engine && typeof engine.pipelineCheck === 'function') {
|
|
239
|
+
r = entryResult(engine.pipelineCheck(JSON.stringify(nodes.map((n) => n.stage))), 'pipelineCheck');
|
|
240
|
+
} else return null;
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
wellTyped: r.wellTyped,
|
|
243
|
+
wireIn: r.wireIn,
|
|
244
|
+
wireOut: r.wireOut,
|
|
245
|
+
mismatch: r.mismatch === null ? null : Object.freeze({ ...r.mismatch }),
|
|
246
|
+
conversions: Object.freeze(r.conversions.map((c) => Object.freeze({ ...c }))),
|
|
247
|
+
assumptions: Object.freeze([...r.assumptions]),
|
|
248
|
+
contract: Object.freeze({ theorem: r.contract.theorem, stages: Object.freeze([...r.contract.stages]) }),
|
|
249
|
+
stages: Object.freeze(r.stages.map((s) => Object.freeze({
|
|
250
|
+
...s,
|
|
251
|
+
assumptions: Object.freeze([...s.assumptions]),
|
|
252
|
+
...(s.outside ? { outside: Object.freeze(s.outside.map((o) => Object.freeze({ ...o }))) } : {}),
|
|
253
|
+
}))),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A pipeline: the nodes in order (pipelines are flattened). `check` is
|
|
259
|
+
* the engine's op pipelineCheck on the nodes' stages; `wellTyped` and
|
|
260
|
+
* `assumptions` (the union of the nodes' named assumptions) are its
|
|
261
|
+
* fields. Without the op: `check` and `wellTyped` are null and
|
|
262
|
+
* `assumptions` is the union of the nodes' fields.
|
|
263
|
+
* @returns {{nodes: object[], assumptions: string[], wellTyped: boolean|null, check: object|null}}
|
|
167
264
|
*/
|
|
168
265
|
function pipe(...items) {
|
|
169
266
|
const nodes = nodesOf(items, 'pipe');
|
|
170
267
|
if (nodes.length === 0) throw new TypeError('pipe: no nodes');
|
|
171
|
-
|
|
268
|
+
const c = check(nodes);
|
|
269
|
+
return Object.freeze({
|
|
270
|
+
nodes: Object.freeze(nodes),
|
|
271
|
+
assumptions: c ? c.assumptions : Object.freeze(unionAssumptions(nodes)),
|
|
272
|
+
wellTyped: c ? c.wellTyped : null,
|
|
273
|
+
check: c,
|
|
274
|
+
});
|
|
172
275
|
}
|
|
173
276
|
|
|
174
277
|
const isWire = (v) => v !== null && typeof v === 'object' && !(v instanceof Dataset) &&
|
|
@@ -180,9 +283,13 @@ export function buildNodes(deps) {
|
|
|
180
283
|
* `options.unchecked: true` lets stages that are not `proved` run,
|
|
181
284
|
* `missing` ones too when the package has a runner for them; each such
|
|
182
285
|
* step's record says so (evidence.mjs). Without it, a `missing` stage
|
|
183
|
-
* refuses before any node runs
|
|
286
|
+
* refuses before any node runs, and so does a pipeline that the engine's
|
|
287
|
+
* check finds not well typed (op pipelineCheck); with it such a pipeline
|
|
288
|
+
* runs and `proved` is false.
|
|
289
|
+
* `assumptions`: the assumptions of the records on the input wire, then
|
|
290
|
+
* the engine's union for this pipeline (op pipelineCheck), no duplicate.
|
|
184
291
|
* @returns {Promise<{graph: any, evidence: object[], assumptions: string[],
|
|
185
|
-
* proved: boolean, uncheckedSteps: string[]}>}
|
|
292
|
+
* proved: boolean, uncheckedSteps: string[], wellTyped: boolean|null}>}
|
|
186
293
|
*/
|
|
187
294
|
async function run(pipeline, input, options) {
|
|
188
295
|
if (options !== undefined && (options === null || typeof options !== 'object')) {
|
|
@@ -194,6 +301,14 @@ export function buildNodes(deps) {
|
|
|
194
301
|
const why = refusalFor(n, unchecked, Object.prototype.hasOwnProperty.call(RUNNERS, n.stage));
|
|
195
302
|
if (why) throw why;
|
|
196
303
|
}
|
|
304
|
+
const c = check(nodes);
|
|
305
|
+
if (c && !c.wellTyped && !unchecked) {
|
|
306
|
+
const m = c.mismatch;
|
|
307
|
+
throw new FactoidalError(`run: the pipeline is not well typed: stage ${m.at} ` +
|
|
308
|
+
`(${nodes[m.at].stage}) writes ${m.out}, stage ${m.at + 1} (${nodes[m.at + 1].stage}) ` +
|
|
309
|
+
`reads ${m.in} (pass { unchecked: true } to run it without a guarantee)`, { code: 'op.failed' });
|
|
310
|
+
}
|
|
311
|
+
const prior = isWire(input) ? input.evidence : [];
|
|
197
312
|
let wire = isWire(input) ? { graph: input.graph, evidence: [...input.evidence] }
|
|
198
313
|
: { graph: input, evidence: [] };
|
|
199
314
|
let owned = null; // a Dataset an earlier node of this run made
|
|
@@ -208,7 +323,17 @@ export function buildNodes(deps) {
|
|
|
208
323
|
}
|
|
209
324
|
wire = { graph, evidence: [...wire.evidence, record] };
|
|
210
325
|
}
|
|
211
|
-
|
|
326
|
+
const s = summarize(wire.evidence);
|
|
327
|
+
if (!c) return { graph: wire.graph, evidence: wire.evidence, ...s, wellTyped: null };
|
|
328
|
+
return {
|
|
329
|
+
graph: wire.graph,
|
|
330
|
+
evidence: wire.evidence,
|
|
331
|
+
assumptions: prior.length === 0 ? [...c.assumptions]
|
|
332
|
+
: unionLists([unionAssumptions(prior), c.assumptions]),
|
|
333
|
+
proved: s.proved && c.wellTyped,
|
|
334
|
+
uncheckedSteps: s.uncheckedSteps,
|
|
335
|
+
wellTyped: c.wellTyped,
|
|
336
|
+
};
|
|
212
337
|
}
|
|
213
338
|
|
|
214
339
|
return { stages, node, pipe, run };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// The LWS reader of an SBM11 generation for lib/sbm11.mjs: the files of a
|
|
2
|
+
// generation (or of a collection root with its CURRENT pointer) stored as
|
|
3
|
+
// Linked Web Storage resources, one resource per file, under one container
|
|
4
|
+
// URL (https://github.com/danbri/factoidal/issues/1010 KR3, read part;
|
|
5
|
+
// LWS 1.0: https://w3c.github.io/lws-protocol/lws10-core/).
|
|
6
|
+
//
|
|
7
|
+
// WHAT THIS FILE IS ALLOWED TO DO
|
|
8
|
+
// Move bytes. The engine names every file and checks every length against
|
|
9
|
+
// the manifest (formal/lean4/L4Factoidal/Storage/Sbm11/HostSession.lean,
|
|
10
|
+
// through the storage medium of formal/lean4/L4Factoidal/Storage/Medium.lean).
|
|
11
|
+
// This file maps a file name to a resource URL, makes HTTP GETs, and hands
|
|
12
|
+
// the bytes over. It reads no manifest and no byte of a file.
|
|
13
|
+
//
|
|
14
|
+
// HTTP (RFC 9110 section 14): each read asks for `Range: bytes=a-b`. A
|
|
15
|
+
// partial answer is used only when it is status 206 and its Content-Range is
|
|
16
|
+
// exactly `bytes a-b/N`. Any other successful answer is the whole
|
|
17
|
+
// representation: it is kept in memory and every later read of that file is
|
|
18
|
+
// cut from it, with no further request. A changed entity tag between two
|
|
19
|
+
// reads of one file refuses the read (the bytes of two versions are never
|
|
20
|
+
// mixed). Limits: no Merkle check here (the engine's Verify.none path), no
|
|
21
|
+
// eviction of the in-memory copies, no retry.
|
|
22
|
+
//
|
|
23
|
+
// EVIDENCE
|
|
24
|
+
// `evidence()` lists each resource read: { resource, etag, lastModified,
|
|
25
|
+
// requests, partial, bytes } -- `partial` the number of 206 answers. The
|
|
26
|
+
// store from openSbm11 answers it as `store.evidence`.
|
|
27
|
+
//
|
|
28
|
+
// SYNCHRONOUS TRANSPORT
|
|
29
|
+
// The engine is inside WebAssembly and asks for bytes synchronously, so the
|
|
30
|
+
// reader needs a synchronous GET: `transport(url, headers) -> { status,
|
|
31
|
+
// header(name) -> string | null, bytes: Uint8Array }`. In Node the default
|
|
32
|
+
// is lib/sync-http-node.mjs (fetch in a worker thread, Atomics.wait); in a
|
|
33
|
+
// browser worker it is a synchronous XMLHttpRequest. A browser window
|
|
34
|
+
// thread has neither: pass `transport` or run the engine in a worker.
|
|
35
|
+
|
|
36
|
+
function containerUrl (url) {
|
|
37
|
+
const s = String(url)
|
|
38
|
+
return s.endsWith('/') ? s : s + '/'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resourceUrl (base, file) {
|
|
42
|
+
return new URL(String(file).split('/').map(encodeURIComponent).join('/'), base).href
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A synchronous GET with XMLHttpRequest (a browser worker). */
|
|
46
|
+
export function xhrSyncTransport (url, headers) {
|
|
47
|
+
const x = new XMLHttpRequest()
|
|
48
|
+
x.open('GET', url, false)
|
|
49
|
+
x.responseType = 'arraybuffer'
|
|
50
|
+
for (const [k, v] of Object.entries(headers)) x.setRequestHeader(k, v)
|
|
51
|
+
x.send()
|
|
52
|
+
return {
|
|
53
|
+
status: x.status,
|
|
54
|
+
header: (name) => x.getResponseHeader(name),
|
|
55
|
+
bytes: new Uint8Array(x.response || new ArrayBuffer(0))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseContentRange (value) {
|
|
60
|
+
const m = /^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/.exec(String(value || '').trim())
|
|
61
|
+
if (!m) return null
|
|
62
|
+
return { first: Number(m[1]), last: Number(m[2]), total: m[3] === '*' ? null : Number(m[3]) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function unsatisfiedTotal (value) {
|
|
66
|
+
const m = /^bytes\s+\*\/(\d+)$/.exec(String(value || '').trim())
|
|
67
|
+
return m ? Number(m[1]) : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A reader ({ readAt, size, evidence, close }) of the SBM11 files held as
|
|
72
|
+
* LWS resources under the container `url`.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} url the container that holds CURRENT and the generation
|
|
75
|
+
* (or the generation container itself)
|
|
76
|
+
* @param {object} [opts]
|
|
77
|
+
* @param {Function} [opts.transport] the synchronous GET (see above)
|
|
78
|
+
* @param {string} [opts.auth] a value for the Authorization header, such as
|
|
79
|
+
* `Bearer <access token>` (an LWS access token)
|
|
80
|
+
* @param {object} [opts.headers] further request headers
|
|
81
|
+
*/
|
|
82
|
+
export function lwsSbm11Reader (url, opts = {}) {
|
|
83
|
+
const base = containerUrl(url)
|
|
84
|
+
let transport = opts.transport
|
|
85
|
+
if (typeof transport !== 'function') {
|
|
86
|
+
if (typeof XMLHttpRequest === 'function' && typeof document === 'undefined') {
|
|
87
|
+
transport = xhrSyncTransport
|
|
88
|
+
} else {
|
|
89
|
+
throw new TypeError('lwsSbm11Reader: no synchronous transport here; pass opts.transport (Node: @factoidal/core gives one)')
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const extra = { ...(opts.headers || {}) }
|
|
93
|
+
if (opts.auth) extra.authorization = String(opts.auth)
|
|
94
|
+
// file -> { url, etag, lastModified, total, whole, requests, partial, bytes }
|
|
95
|
+
const files = new Map()
|
|
96
|
+
const errors = []
|
|
97
|
+
|
|
98
|
+
function entry (file) {
|
|
99
|
+
let e = files.get(file)
|
|
100
|
+
if (!e) {
|
|
101
|
+
e = { url: resourceUrl(base, file), etag: null, lastModified: null, total: undefined, whole: null, requests: 0, partial: 0, bytes: 0 }
|
|
102
|
+
files.set(file, e)
|
|
103
|
+
}
|
|
104
|
+
return e
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fail (file, message) {
|
|
108
|
+
errors.push(`${file}: ${message}`)
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function get (e, file, first, last) {
|
|
113
|
+
const headers = { ...extra, range: `bytes=${first}-${last}` }
|
|
114
|
+
let r
|
|
115
|
+
try {
|
|
116
|
+
r = transport(e.url, headers)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
fail(file, `GET ${e.url}: ${error && error.message ? error.message : error}`)
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
e.requests += 1
|
|
122
|
+
const tag = r.header('etag')
|
|
123
|
+
if (r.status >= 200 && r.status < 300) {
|
|
124
|
+
if (e.etag !== null && tag !== e.etag) {
|
|
125
|
+
fail(file, `the entity tag changed from ${e.etag} to ${tag}`)
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
e.etag = tag
|
|
129
|
+
e.lastModified = r.header('last-modified')
|
|
130
|
+
e.bytes += r.bytes.length
|
|
131
|
+
}
|
|
132
|
+
return r
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sizeOf (file) {
|
|
136
|
+
const e = entry(file)
|
|
137
|
+
if (e.total !== undefined) return e.total
|
|
138
|
+
const r = get(e, file, 0, 0)
|
|
139
|
+
if (r === null) return null
|
|
140
|
+
const cr = parseContentRange(r.header('content-range'))
|
|
141
|
+
if (r.status === 206 && cr && cr.first === 0 && cr.last === 0 && cr.total !== null && r.bytes.length === 1) {
|
|
142
|
+
e.partial += 1
|
|
143
|
+
e.total = cr.total
|
|
144
|
+
} else if (r.status === 416 && unsatisfiedTotal(r.header('content-range')) !== null) {
|
|
145
|
+
e.total = unsatisfiedTotal(r.header('content-range'))
|
|
146
|
+
e.whole = new Uint8Array(0)
|
|
147
|
+
} else if (r.status === 200) {
|
|
148
|
+
e.whole = r.bytes
|
|
149
|
+
e.total = r.bytes.length
|
|
150
|
+
} else if (r.status === 404 || r.status === 410) {
|
|
151
|
+
e.total = null
|
|
152
|
+
} else {
|
|
153
|
+
fail(file, `GET ${e.url}: status ${r.status}`)
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
return e.total
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
readAt (file, offset, length) {
|
|
161
|
+
const e = entry(file)
|
|
162
|
+
if (length === 0) return new Uint8Array(0)
|
|
163
|
+
if (e.whole !== null) {
|
|
164
|
+
return offset + length <= e.whole.length ? e.whole.subarray(offset, offset + length) : null
|
|
165
|
+
}
|
|
166
|
+
const last = offset + length - 1
|
|
167
|
+
const r = get(e, file, offset, last)
|
|
168
|
+
if (r === null) return null
|
|
169
|
+
const cr = parseContentRange(r.header('content-range'))
|
|
170
|
+
if (r.status === 206 && cr && cr.first === offset && cr.last === last && r.bytes.length === length) {
|
|
171
|
+
e.partial += 1
|
|
172
|
+
return r.bytes
|
|
173
|
+
}
|
|
174
|
+
if (r.status === 200) {
|
|
175
|
+
e.whole = r.bytes
|
|
176
|
+
e.total = r.bytes.length
|
|
177
|
+
return offset + length <= r.bytes.length ? r.bytes.subarray(offset, offset + length) : null
|
|
178
|
+
}
|
|
179
|
+
return fail(file, `GET ${e.url} bytes ${offset}-${last}: status ${r.status}`)
|
|
180
|
+
},
|
|
181
|
+
size (file) {
|
|
182
|
+
return sizeOf(file)
|
|
183
|
+
},
|
|
184
|
+
/** Each resource read: { resource, etag, lastModified, requests, partial, bytes }. */
|
|
185
|
+
evidence () {
|
|
186
|
+
const out = []
|
|
187
|
+
for (const e of files.values()) {
|
|
188
|
+
if (e.requests === 0 || e.etag === null) continue
|
|
189
|
+
out.push({ resource: e.url, etag: e.etag, lastModified: e.lastModified, requests: e.requests, partial: e.partial, bytes: e.bytes })
|
|
190
|
+
}
|
|
191
|
+
return out
|
|
192
|
+
},
|
|
193
|
+
/** The transport failures, as text (a failed read reaches the engine as no bytes). */
|
|
194
|
+
errors () { return errors.slice() },
|
|
195
|
+
close () {
|
|
196
|
+
files.clear()
|
|
197
|
+
if (typeof opts.onClose === 'function') opts.onClose()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
package/lib/sbm11.mjs
CHANGED
|
@@ -68,6 +68,32 @@ export function setDefaultSbm11Reader (factory) {
|
|
|
68
68
|
defaultReader = factory
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A reader over files held in memory: `files` is a Map (or a plain object)
|
|
73
|
+
* from file name to Uint8Array, for example the result of packSbm11. It
|
|
74
|
+
* moves bytes only; a read past the end of a file, or of a file it does not
|
|
75
|
+
* hold, answers null, and the engine refuses it with the file name.
|
|
76
|
+
*/
|
|
77
|
+
export function mapReader (files) {
|
|
78
|
+
const m = files instanceof Map ? files : new Map(Object.entries(files || {}))
|
|
79
|
+
return {
|
|
80
|
+
readAt (file, offset, length) {
|
|
81
|
+
const b = m.get(file)
|
|
82
|
+
if (!b || offset < 0 || length < 0 || offset + length > b.length) return null
|
|
83
|
+
return b.subarray(offset, offset + length)
|
|
84
|
+
},
|
|
85
|
+
size (file) {
|
|
86
|
+
const b = m.get(file)
|
|
87
|
+
return b ? b.length : null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function opsReflection (eng) {
|
|
93
|
+
const r = eng.call('ops', [])
|
|
94
|
+
return typeof r === 'string' ? JSON.parse(r) : r
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
function envelope (raw) {
|
|
72
98
|
const j = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
73
99
|
if (!j || j.ok !== true) {
|
|
@@ -98,8 +124,15 @@ export function sbm11Api (loadEngine) {
|
|
|
98
124
|
const opened = envelope(eng.call('sbm11Open', [String(source)]))
|
|
99
125
|
const handle = opened.handle
|
|
100
126
|
let open = true
|
|
127
|
+
const reader = options.reader || readers.get(String(source))
|
|
101
128
|
return {
|
|
102
129
|
handle,
|
|
130
|
+
/** The resources the reader read (`lwsSbm11Reader`): `[{ resource,
|
|
131
|
+
* etag, lastModified, requests, partial, bytes }]`; undefined for a
|
|
132
|
+
* reader that records none. */
|
|
133
|
+
get evidence () {
|
|
134
|
+
return reader && typeof reader.evidence === 'function' ? reader.evidence() : undefined
|
|
135
|
+
},
|
|
103
136
|
generation: opened.generation,
|
|
104
137
|
terms: Number(opened.terms),
|
|
105
138
|
query (sparql, qopts = {}) {
|
|
@@ -144,5 +177,44 @@ export function sbm11Api (loadEngine) {
|
|
|
144
177
|
}
|
|
145
178
|
}
|
|
146
179
|
}
|
|
147
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Pack an RDF document into the files of one SBM11 generation, in memory
|
|
182
|
+
* (the wasm op sbm11PackFiles, formal/lean4/Wasm/Ops/Sbm11Pack.lean;
|
|
183
|
+
* https://github.com/danbri/factoidal/issues/1009 stage 2). `text` is a
|
|
184
|
+
* string or UTF-8 bytes; `options.syntax` is turtle, trig, nquads,
|
|
185
|
+
* ntriples, rdfxml or jsonld; `options.base` the base IRI. Resolves to a
|
|
186
|
+
* Map from file name to Uint8Array: the files `l4sbm11-pack` writes for the
|
|
187
|
+
* same quads (data files, `.merkle` sidecars, `manifest.sbm11`), without
|
|
188
|
+
* `CURRENT`. Open them with `openSbm11(name, { reader: mapReader(files) })`,
|
|
189
|
+
* or write each under its name into one directory. The engine names every
|
|
190
|
+
* file and computes every byte (iron rule 7).
|
|
191
|
+
*/
|
|
192
|
+
async function packSbm11 (text, options = {}) {
|
|
193
|
+
const syntax = options.syntax ?? options.format
|
|
194
|
+
if (typeof syntax !== 'string' || syntax === '') {
|
|
195
|
+
throw new TypeError('packSbm11: options.syntax is required (turtle, trig, nquads, ntriples, rdfxml or jsonld)')
|
|
196
|
+
}
|
|
197
|
+
const eng = await loadEngine()
|
|
198
|
+
const refl = opsReflection(eng)
|
|
199
|
+
const blobIoOps = Array.isArray(refl && refl.blobIoOps) ? refl.blobIoOps : []
|
|
200
|
+
if (typeof eng.callBlobIO !== 'function' || !blobIoOps.includes('sbm11PackFiles')) {
|
|
201
|
+
const e = new Error('packSbm11: this wasm module has no sbm11PackFiles op (it was built before ' +
|
|
202
|
+
'https://github.com/danbri/factoidal/issues/1009 stage 2)')
|
|
203
|
+
e.code = 'SBM11_NO_PACK_OP'
|
|
204
|
+
throw e
|
|
205
|
+
}
|
|
206
|
+
const bytes = typeof text === 'string' ? new TextEncoder().encode(text) : new Uint8Array(text)
|
|
207
|
+
const base = options.base == null ? '' : String(options.base)
|
|
208
|
+
const files = new Map()
|
|
209
|
+
let count = 1
|
|
210
|
+
for (let i = 0; i < count; i++) {
|
|
211
|
+
const answer = eng.callBlobIO('sbm11PackFiles', [syntax, base, String(i)], bytes)
|
|
212
|
+
const j = envelope(answer.envelope)
|
|
213
|
+
count = Number(j.count)
|
|
214
|
+
files.set(j.name, answer.bytes)
|
|
215
|
+
}
|
|
216
|
+
return files
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { openSbm11, packSbm11 }
|
|
148
220
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// A synchronous HTTP GET for Node, for the readers of lib/sbm11.mjs that read
|
|
2
|
+
// over HTTP (lib/sbm11-lws.mjs; https://github.com/danbri/factoidal/issues/1010).
|
|
3
|
+
// The engine is inside WebAssembly and asks for bytes synchronously; Node's
|
|
4
|
+
// fetch is asynchronous. A worker thread makes the fetch; this thread waits
|
|
5
|
+
// on a shared Int32Array (Atomics.wait) and takes the answer with
|
|
6
|
+
// receiveMessageOnPort. Pure I/O: no header is read here, no status judged.
|
|
7
|
+
//
|
|
8
|
+
// The thread that calls the transport is blocked until the answer comes, so
|
|
9
|
+
// the server must not run on that thread (start it in another process or
|
|
10
|
+
// worker).
|
|
11
|
+
|
|
12
|
+
import { Worker, MessageChannel, receiveMessageOnPort } from 'node:worker_threads'
|
|
13
|
+
|
|
14
|
+
const WORKER = `
|
|
15
|
+
const { workerData } = require('node:worker_threads')
|
|
16
|
+
const { port, signal } = workerData
|
|
17
|
+
const flag = new Int32Array(signal)
|
|
18
|
+
port.on('message', async (req) => {
|
|
19
|
+
let out
|
|
20
|
+
try {
|
|
21
|
+
const r = await fetch(req.url, { headers: req.headers, redirect: 'follow' })
|
|
22
|
+
const headers = {}
|
|
23
|
+
r.headers.forEach((v, k) => { headers[k] = v })
|
|
24
|
+
out = { status: r.status, headers, bytes: new Uint8Array(await r.arrayBuffer()) }
|
|
25
|
+
} catch (e) {
|
|
26
|
+
out = { error: String(e && e.message ? e.message : e) }
|
|
27
|
+
}
|
|
28
|
+
port.postMessage(out)
|
|
29
|
+
Atomics.store(flag, 0, 1)
|
|
30
|
+
Atomics.notify(flag, 0)
|
|
31
|
+
})
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A synchronous transport `(url, headers) -> { status, header(name), bytes }`
|
|
36
|
+
* with a `close()` that stops its worker.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} [opts]
|
|
39
|
+
* @param {number} [opts.timeoutMs] the most one request may take (default 60000)
|
|
40
|
+
*/
|
|
41
|
+
export function nodeSyncTransport (opts = {}) {
|
|
42
|
+
const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : 60000
|
|
43
|
+
const signal = new SharedArrayBuffer(4)
|
|
44
|
+
const flag = new Int32Array(signal)
|
|
45
|
+
const { port1, port2 } = new MessageChannel()
|
|
46
|
+
const worker = new Worker(WORKER, { eval: true, workerData: { port: port2, signal }, transferList: [port2] })
|
|
47
|
+
worker.unref()
|
|
48
|
+
port1.unref()
|
|
49
|
+
let closed = false
|
|
50
|
+
function transport (url, headers) {
|
|
51
|
+
if (closed) throw new Error('the transport is closed')
|
|
52
|
+
Atomics.store(flag, 0, 0)
|
|
53
|
+
port1.postMessage({ url: String(url), headers: { ...headers } })
|
|
54
|
+
if (Atomics.wait(flag, 0, 0, timeoutMs) === 'timed-out') {
|
|
55
|
+
throw new Error(`GET ${url}: no answer in ${timeoutMs} ms`)
|
|
56
|
+
}
|
|
57
|
+
const got = receiveMessageOnPort(port1)
|
|
58
|
+
if (!got) throw new Error(`GET ${url}: no answer from the transport worker`)
|
|
59
|
+
const m = got.message
|
|
60
|
+
if (m.error) throw new Error(m.error)
|
|
61
|
+
const h = m.headers
|
|
62
|
+
return { status: m.status, header: (name) => (Object.hasOwn(h, name.toLowerCase()) ? h[name.toLowerCase()] : null), bytes: m.bytes }
|
|
63
|
+
}
|
|
64
|
+
transport.close = () => {
|
|
65
|
+
if (closed) return
|
|
66
|
+
closed = true
|
|
67
|
+
port1.close()
|
|
68
|
+
worker.terminate()
|
|
69
|
+
}
|
|
70
|
+
return transport
|
|
71
|
+
}
|
package/lib/version.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factoidal/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "RDF parsing and writing (Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD), SPARQL 1.1 and 1.2, RDFS and OWL reasoning, SHACL and ShEx validation, CSVW and RML mapping, RDFC-1.0, and SPARQL, LWS and Solid servers, for Node, Deno and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sparql",
|