@factoidal/core 0.1.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 +121 -0
- package/LICENSE +201 -0
- package/README.md +514 -0
- package/browser-wasm.js +872 -0
- package/browser.d.ts +514 -0
- package/browser.js +2276 -0
- package/factoidal-npm-entry.js +32609 -0
- package/factoidal-npm-entry.wasm.assets/code-7ac046580f1bbdda8dc6.wasm +0 -0
- package/factoidal-npm-entry.wasm.js +455 -0
- package/factoidal.js +27560 -0
- package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
- package/factoidal.wasm.js +457 -0
- package/fn.d.ts +519 -0
- package/fn.js +916 -0
- package/hacl-init.js +92 -0
- package/hacl-wasm/FStar.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
- package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
- package/hacl-wasm/LowStar_Endianness.wasm +0 -0
- package/hacl-wasm/WasmSupport.wasm +0 -0
- package/hacl-wasm/api.js +775 -0
- package/hacl-wasm/api.json +3787 -0
- package/hacl-wasm/layouts.json +1 -0
- package/hacl-wasm/loader.js +568 -0
- package/hacl-wasm/shell.js +12 -0
- package/index.d.ts +1068 -0
- package/index.js +237 -0
- package/index.mjs +85 -0
- package/lib/api.js +2140 -0
- package/lib/engine-js.js +165 -0
- package/lib/engine-wasm.js +300 -0
- package/package.json +101 -0
- package/rdfjs.js +540 -0
- package/version.json +101 -0
- package/wasm.d.ts +130 -0
- package/wasm.js +158 -0
package/browser-wasm.js
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
// factoidal — browser ESM entry for the **wasm** bundle.
|
|
2
|
+
//
|
|
3
|
+
// This is the wasm_of_ocaml-compiled sibling of ./browser.js. Same
|
|
4
|
+
// async API; different extraction target under the hood. Use this
|
|
5
|
+
// when you need the smaller/faster wasm path (Chrome >= 119,
|
|
6
|
+
// Edge >= 119, Node >= 22; Firefox needs Wasm-GC to ship before
|
|
7
|
+
// this works there).
|
|
8
|
+
//
|
|
9
|
+
// <script type="module">
|
|
10
|
+
// import { query } from 'https://unpkg.com/factoidal/browser-wasm.js';
|
|
11
|
+
// const r = await query(dataTtl,
|
|
12
|
+
// 'SELECT * WHERE { ?s ?p ?o }',
|
|
13
|
+
// { entail: 'RDFS' }
|
|
14
|
+
// );
|
|
15
|
+
// console.log(r.results.bindings);
|
|
16
|
+
// </script>
|
|
17
|
+
//
|
|
18
|
+
// --- How we drive the wasm bundle ---
|
|
19
|
+
//
|
|
20
|
+
// `factoidal.wasm.js` is a single top-level `(async () => ...)(...)`
|
|
21
|
+
// expression emitted by wasm_of_ocaml. It inspects
|
|
22
|
+
// `globalThis.process.versions.node` to decide between two paths:
|
|
23
|
+
//
|
|
24
|
+
// - Node path: uses `require("node:fs")` + `require("node:path")` +
|
|
25
|
+
// `require("node:fs/promises")` to write stdout, resolve the
|
|
26
|
+
// `.wasm` asset, and (critically) reads CLI args from
|
|
27
|
+
// `globalThis.process.argv`. All real I/O — including the data
|
|
28
|
+
// file at `-d /static/data.ttl` — goes through `f.openSync`
|
|
29
|
+
// etc. NB: unlike the non-wasm bundle, **there is no MlFakeDevice**;
|
|
30
|
+
// `globalThis.jsoo_fs_tmp` is not consulted.
|
|
31
|
+
//
|
|
32
|
+
// - Browser path: uses `fetch()` to load the `.wasm` asset and
|
|
33
|
+
// falls back to `console.log` / `console.error` for stdout /
|
|
34
|
+
// stderr. Argv defaults to `["a.out"]` — so we can't drive the
|
|
35
|
+
// CLI at all through the browser branch. For a CLI-shaped
|
|
36
|
+
// entry-point we have to keep the Node branch and shim
|
|
37
|
+
// `require()` + `globalThis.process` ourselves.
|
|
38
|
+
//
|
|
39
|
+
// We therefore take the **Node branch in every environment** and
|
|
40
|
+
// inject a fake `require()` + `process` that back everything with
|
|
41
|
+
// in-memory buffers:
|
|
42
|
+
//
|
|
43
|
+
// - fake `require("node:fs")` routes `writeSync(1, ...)` and
|
|
44
|
+
// `writeSync(2, ...)` into per-call stdout/stderr arrays;
|
|
45
|
+
// - `openSync`/`readSync`/`fstatSync`/`existsSync` serve up the
|
|
46
|
+
// single `/static/data.<ext>` entry seeded from `dataString`;
|
|
47
|
+
// - fake `require("node:fs/promises")` reads the `.wasm` asset
|
|
48
|
+
// via `fetch()` under a real browser or via the injected
|
|
49
|
+
// `_setWasmAssetFallback()` hook under Node.
|
|
50
|
+
//
|
|
51
|
+
// Finally, the wasm_of_ocaml IIFE's top-level expression is a
|
|
52
|
+
// fire-and-forget async call — its Promise is *discarded* at the
|
|
53
|
+
// source level. We inject a single tiny rewrite
|
|
54
|
+
// (`;($=>async ` -> `;globalThis.__fwPromise=($=>async `) to
|
|
55
|
+
// capture it, then `await` it and delete it. Without this the
|
|
56
|
+
// `new Function(src)()` returns `undefined` before the wasm module
|
|
57
|
+
// has even finished loading.
|
|
58
|
+
|
|
59
|
+
const DEFAULT_WASM_URL = new URL('./factoidal.wasm.js', import.meta.url).href;
|
|
60
|
+
|
|
61
|
+
let _wasmJsUrl = DEFAULT_WASM_URL;
|
|
62
|
+
let _wasmJsSrc = null; // cached transformed source
|
|
63
|
+
let _fetchPromise = null;
|
|
64
|
+
let _nodeFsFallback = null; // test-only: (assetSubPath) => Buffer|Uint8Array
|
|
65
|
+
|
|
66
|
+
// The bundle's entry is an immediately-invoked async factory:
|
|
67
|
+
// ;(<param>=>async <arg>=>{...})(...)
|
|
68
|
+
// wasm_of_ocaml minifies <param> differently across versions ('$'
|
|
69
|
+
// before mid-2026, 'ag' in 6.4.1) - match the shape, not a fixed
|
|
70
|
+
// name, and splice the __fwPromise capture in after the leading ';'.
|
|
71
|
+
// (Synced from docs/fstar-extracted/browser-wasm.js, which carried
|
|
72
|
+
// this fix first — the npm package copy had drifted behind it.)
|
|
73
|
+
const IIFE_RE = /;\((\$|[A-Za-z_$][\w$]*)=>async /;
|
|
74
|
+
const IIFE_CAPTURE = ';globalThis.__fwPromise=';
|
|
75
|
+
|
|
76
|
+
function rewriteBundle(src) {
|
|
77
|
+
const m = IIFE_RE.exec(src);
|
|
78
|
+
if (!m) {
|
|
79
|
+
// Unexpected bundle shape. Bail loudly.
|
|
80
|
+
throw new Error(
|
|
81
|
+
"browser-wasm: could not locate the async IIFE marker in " +
|
|
82
|
+
"factoidal.wasm.js. The bundle shape may have changed."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return src.slice(0, m.index) + IIFE_CAPTURE + src.slice(m.index + 1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Override where `factoidal.wasm.js` is loaded from. The `.wasm`
|
|
90
|
+
* asset in `factoidal.wasm.assets/` is resolved relative to this URL.
|
|
91
|
+
*/
|
|
92
|
+
export function setFactoidalWasmUrl(url) {
|
|
93
|
+
_wasmJsUrl = url;
|
|
94
|
+
_wasmJsSrc = null;
|
|
95
|
+
_fetchPromise = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Current `factoidal.wasm.js` source URL. Mirrors browser.js's
|
|
100
|
+
* `getFactoidalUrl()` — lets a caller check before overriding so it
|
|
101
|
+
* only pays the cache-reset cost when the URL actually changes.
|
|
102
|
+
*/
|
|
103
|
+
export function getFactoidalWasmUrl() {
|
|
104
|
+
return _wasmJsUrl;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Test-only hook. Lets the Node smoke test inject an fs-based
|
|
109
|
+
* reader for the .wasm asset so we don't have to polyfill fetch()
|
|
110
|
+
* across Node versions. Pass `null` to clear.
|
|
111
|
+
* _setWasmAssetFallback((assetSubPath) => fs.readFileSync(...))
|
|
112
|
+
*/
|
|
113
|
+
export function _setWasmAssetFallback(reader) {
|
|
114
|
+
_nodeFsFallback = reader;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Test-only hook to seed the cached (already-rewritten) source
|
|
119
|
+
* without going through fetch(). Used by the Node smoke test.
|
|
120
|
+
*/
|
|
121
|
+
export function _setFactoidalWasmSource(src) {
|
|
122
|
+
_wasmJsSrc = rewriteBundle(src);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function loadFactoidalWasmSource() {
|
|
126
|
+
if (_wasmJsSrc) return Promise.resolve(_wasmJsSrc);
|
|
127
|
+
if (_fetchPromise) return _fetchPromise;
|
|
128
|
+
_fetchPromise = fetch(_wasmJsUrl)
|
|
129
|
+
.then((r) => {
|
|
130
|
+
if (!r.ok) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`factoidal.wasm.js fetch failed: ${r.status} ${r.statusText}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return r.text();
|
|
136
|
+
})
|
|
137
|
+
.then((text) => { _wasmJsSrc = rewriteBundle(text); return _wasmJsSrc; });
|
|
138
|
+
return _fetchPromise;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const DATA_FORMAT_EXT = {
|
|
142
|
+
turtle: 'ttl',
|
|
143
|
+
ttl: 'ttl',
|
|
144
|
+
ntriples: 'nt',
|
|
145
|
+
nt: 'nt',
|
|
146
|
+
nquads: 'nq',
|
|
147
|
+
nq: 'nq',
|
|
148
|
+
trig: 'trig',
|
|
149
|
+
rdfxml: 'rdf',
|
|
150
|
+
'rdf-xml': 'rdf',
|
|
151
|
+
rdf: 'rdf',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const OUTPUT_FORMATS = new Set(['json', 'csv', 'tsv', 'xml', 'table', 'ntriples']);
|
|
155
|
+
const ENTAIL_VALUES = new Set(['none', 'RDFS', 'OWL-RL', 'x-rdfscore', 'x-rdfsplus']);
|
|
156
|
+
|
|
157
|
+
function extForFormat(fmt) {
|
|
158
|
+
const key = String(fmt || 'turtle').toLowerCase();
|
|
159
|
+
if (!(key in DATA_FORMAT_EXT)) {
|
|
160
|
+
throw new TypeError(
|
|
161
|
+
`Unknown dataFormat '${fmt}'. Expected one of: ` +
|
|
162
|
+
Object.keys(DATA_FORMAT_EXT).join(', ')
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return DATA_FORMAT_EXT[key];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function assetSubPath(urlLike) {
|
|
169
|
+
const s = typeof urlLike === 'string'
|
|
170
|
+
? urlLike
|
|
171
|
+
: (urlLike && urlLike.url) || String(urlLike);
|
|
172
|
+
const marker = 'factoidal.wasm.assets/';
|
|
173
|
+
const ix = s.lastIndexOf(marker);
|
|
174
|
+
return ix < 0 ? s : s.slice(ix);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Small helper: reify a Node-ish Stats object (with optional
|
|
178
|
+
// bigint mode) for our fake files. Real Node returns BigInt fields
|
|
179
|
+
// when called as `statSync(p, { bigint: true })`; the wasm loader
|
|
180
|
+
// uses that flag for `file_size`.
|
|
181
|
+
function makeStat(size, isFile, opts) {
|
|
182
|
+
const bigint = !!(opts && opts.bigint);
|
|
183
|
+
const toN = (v) => bigint ? BigInt(v) : v;
|
|
184
|
+
return {
|
|
185
|
+
isFile: () => isFile,
|
|
186
|
+
isDirectory: () => false,
|
|
187
|
+
isCharacterDevice: () => !isFile,
|
|
188
|
+
isBlockDevice: () => false,
|
|
189
|
+
isSymbolicLink: () => false,
|
|
190
|
+
isFIFO: () => false,
|
|
191
|
+
isSocket: () => false,
|
|
192
|
+
dev: toN(0), ino: toN(0), mode: toN(0o100644),
|
|
193
|
+
nlink: toN(1), uid: toN(0), gid: toN(0), rdev: toN(0),
|
|
194
|
+
size: toN(size),
|
|
195
|
+
atimeMs: bigint ? 0n : 0,
|
|
196
|
+
mtimeMs: bigint ? 0n : 0,
|
|
197
|
+
ctimeMs: bigint ? 0n : 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Run a SPARQL query against an RDF dataset in memory, using the
|
|
203
|
+
* wasm_of_ocaml bundle.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} dataString
|
|
206
|
+
* @param {string} queryString
|
|
207
|
+
* @param {object} [options]
|
|
208
|
+
* @param {string} [options.dataFormat='turtle']
|
|
209
|
+
* @param {string} [options.entail='none']
|
|
210
|
+
* @param {string} [options.output='json']
|
|
211
|
+
* @returns {Promise<object|string>}
|
|
212
|
+
*/
|
|
213
|
+
export async function query(dataString, queryString, options) {
|
|
214
|
+
if (typeof dataString !== 'string') {
|
|
215
|
+
throw new TypeError('query: dataString must be a string');
|
|
216
|
+
}
|
|
217
|
+
if (typeof queryString !== 'string') {
|
|
218
|
+
throw new TypeError('query: queryString must be a string');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const opts = options || {};
|
|
222
|
+
const dataFormat = opts.dataFormat || 'turtle';
|
|
223
|
+
const entail = opts.entail || 'none';
|
|
224
|
+
const output = opts.output || 'json';
|
|
225
|
+
|
|
226
|
+
if (!ENTAIL_VALUES.has(entail)) {
|
|
227
|
+
throw new TypeError(
|
|
228
|
+
`query: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (!OUTPUT_FORMATS.has(output)) {
|
|
232
|
+
throw new TypeError(
|
|
233
|
+
`query: output must be one of ${[...OUTPUT_FORMATS].join(', ')}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const src = await loadFactoidalWasmSource();
|
|
238
|
+
|
|
239
|
+
const ext = extForFormat(dataFormat);
|
|
240
|
+
const dataPath = '/static/data.' + ext;
|
|
241
|
+
|
|
242
|
+
// Seed the fake filesystem with just the one input file.
|
|
243
|
+
const TE = new TextEncoder();
|
|
244
|
+
const dataBytes = TE.encode(dataString);
|
|
245
|
+
|
|
246
|
+
// Preserve anything we're about to overwrite so we can roll back
|
|
247
|
+
// cleanly — even on throw.
|
|
248
|
+
const orig = {
|
|
249
|
+
proc: globalThis.process,
|
|
250
|
+
jsooFs: globalThis.jsoo_fs_tmp,
|
|
251
|
+
fetch: globalThis.fetch,
|
|
252
|
+
require: globalThis.require,
|
|
253
|
+
fwPromise: globalThis.__fwPromise,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const stdoutBuf = [];
|
|
257
|
+
const stderrBuf = [];
|
|
258
|
+
const pushOut = (chunk) => stdoutBuf.push(chunk);
|
|
259
|
+
const pushErr = (chunk) => stderrBuf.push(chunk);
|
|
260
|
+
|
|
261
|
+
const argv = [
|
|
262
|
+
'node', 'factoidal',
|
|
263
|
+
'-d', dataPath,
|
|
264
|
+
'-e', queryString,
|
|
265
|
+
'-o', output === 'json' ? 'json' : output,
|
|
266
|
+
];
|
|
267
|
+
if (entail !== 'none') argv.push('--entail', entail);
|
|
268
|
+
|
|
269
|
+
let exitCode = 0;
|
|
270
|
+
const EXIT_SENTINEL = new Error('__factoidal_exit__');
|
|
271
|
+
EXIT_SENTINEL.__factoidalExit = true;
|
|
272
|
+
|
|
273
|
+
// ---- fake fs (single-file, read-only, /static/data.<ext>) ----
|
|
274
|
+
const TD = new TextDecoder('utf-8');
|
|
275
|
+
const fakeOpenFds = Object.create(null);
|
|
276
|
+
let nextFd = 100;
|
|
277
|
+
|
|
278
|
+
function writeSync(fd, buf, offset, length /*, position */) {
|
|
279
|
+
let s;
|
|
280
|
+
if (typeof buf === 'string') {
|
|
281
|
+
s = buf;
|
|
282
|
+
} else {
|
|
283
|
+
const u8 = (buf instanceof Uint8Array)
|
|
284
|
+
? buf
|
|
285
|
+
: new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
286
|
+
const start = offset || 0;
|
|
287
|
+
const end = start + (length === undefined ? u8.length - start : length);
|
|
288
|
+
s = TD.decode(u8.subarray(start, end));
|
|
289
|
+
}
|
|
290
|
+
(fd === 2 ? pushErr : pushOut)(s);
|
|
291
|
+
return length === undefined ? s.length : length;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function fileForPath(p) {
|
|
295
|
+
if (p === dataPath) return dataBytes;
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const fakeFs = {
|
|
300
|
+
constants: {
|
|
301
|
+
R_OK: 4, W_OK: 2, X_OK: 1, F_OK: 0,
|
|
302
|
+
O_RDONLY: 0, O_WRONLY: 1, O_RDWR: 2, O_APPEND: 8, O_CREAT: 512,
|
|
303
|
+
O_TRUNC: 1024, O_EXCL: 2048, O_NONBLOCK: 4096, O_NOCTTY: 8192,
|
|
304
|
+
O_DSYNC: 4194304, O_SYNC: 128,
|
|
305
|
+
},
|
|
306
|
+
writeSync,
|
|
307
|
+
openSync: (p /*, flags, mode */) => {
|
|
308
|
+
const f = fileForPath(String(p));
|
|
309
|
+
if (!f) {
|
|
310
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${p}'`),
|
|
311
|
+
{ code: 'ENOENT', errno: -2, syscall: 'open', path: p });
|
|
312
|
+
}
|
|
313
|
+
const fd = nextFd++;
|
|
314
|
+
fakeOpenFds[fd] = { path: String(p), buf: f, offset: 0 };
|
|
315
|
+
return fd;
|
|
316
|
+
},
|
|
317
|
+
closeSync: (fd) => { delete fakeOpenFds[fd]; },
|
|
318
|
+
readSync: (fd, buf, offset, length, position) => {
|
|
319
|
+
const f = fakeOpenFds[fd];
|
|
320
|
+
if (!f) throw Object.assign(new Error('EBADF'), { code: 'EBADF', errno: -9 });
|
|
321
|
+
const pos = (position === null || position === undefined) ? f.offset : Number(position);
|
|
322
|
+
const remaining = Math.max(0, f.buf.length - pos);
|
|
323
|
+
const n = Math.min(length, remaining);
|
|
324
|
+
for (let i = 0; i < n; i++) buf[offset + i] = f.buf[pos + i];
|
|
325
|
+
if (position === null || position === undefined) f.offset = pos + n;
|
|
326
|
+
return n;
|
|
327
|
+
},
|
|
328
|
+
fsyncSync: () => {},
|
|
329
|
+
existsSync: (p) => fileForPath(String(p)) !== null,
|
|
330
|
+
accessSync: (p) => {
|
|
331
|
+
if (!fileForPath(String(p))) {
|
|
332
|
+
throw Object.assign(new Error(`ENOENT`), { code: 'ENOENT', errno: -2 });
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
statSync: (p, o) => {
|
|
336
|
+
const f = fileForPath(String(p));
|
|
337
|
+
if (!f) {
|
|
338
|
+
if (o && o.throwIfNoEntry === false) return undefined;
|
|
339
|
+
throw Object.assign(new Error(`ENOENT`), { code: 'ENOENT', errno: -2 });
|
|
340
|
+
}
|
|
341
|
+
return makeStat(f.length, true, o);
|
|
342
|
+
},
|
|
343
|
+
lstatSync: (p, o) => fakeFs.statSync(p, o),
|
|
344
|
+
fstatSync: (fd, o) => {
|
|
345
|
+
const f = fakeOpenFds[fd];
|
|
346
|
+
return makeStat(f ? f.buf.length : 0, !!f, o);
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Resolve the .wasm asset: prefer the test-injected reader, else
|
|
351
|
+
// fall back to a fetch() relative to _wasmJsUrl.
|
|
352
|
+
async function readWasmAsset(requestedPath) {
|
|
353
|
+
const sub = assetSubPath(String(requestedPath));
|
|
354
|
+
if (_nodeFsFallback) {
|
|
355
|
+
const bytes = await _nodeFsFallback(sub);
|
|
356
|
+
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
357
|
+
}
|
|
358
|
+
const resolved = new URL(sub, _wasmJsUrl).href;
|
|
359
|
+
const fetchFn = orig.fetch || globalThis.fetch;
|
|
360
|
+
if (!fetchFn) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
"browser-wasm: no fetch() available and no _setWasmAssetFallback() " +
|
|
363
|
+
"injected. In Node <20 or an exotic runtime, set a fallback reader."
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const r = await fetchFn(resolved);
|
|
367
|
+
if (!r.ok) {
|
|
368
|
+
throw new Error(`wasm asset fetch failed: ${r.status} ${r.statusText} (${resolved})`);
|
|
369
|
+
}
|
|
370
|
+
return new Uint8Array(await r.arrayBuffer());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const fakeRequire = (name) => {
|
|
374
|
+
if (name === 'node:fs') return fakeFs;
|
|
375
|
+
if (name === 'node:fs/promises') return { readFile: readWasmAsset };
|
|
376
|
+
if (name === 'node:path') return {
|
|
377
|
+
join: (...parts) => parts.filter(Boolean).join('/').replace(/\/+/g, '/'),
|
|
378
|
+
dirname: (p) => String(p).replace(/\/[^/]*$/, '') || '.',
|
|
379
|
+
};
|
|
380
|
+
if (name === 'node:os') return { tmpdir: () => '/tmp' };
|
|
381
|
+
if (name === 'node:tty') return { isatty: () => false };
|
|
382
|
+
if (name === 'node:child_process') return { spawnSync: () => ({ status: 0, signal: null }) };
|
|
383
|
+
throw new Error(`browser-wasm shim: unexpected require('${name}')`);
|
|
384
|
+
};
|
|
385
|
+
// `require.main.filename` is read by the bundle. Its `dirname`
|
|
386
|
+
// gets joined onto `factoidal.wasm.assets/foo.wasm`, which we
|
|
387
|
+
// then strip back to the subpath inside readWasmAsset.
|
|
388
|
+
fakeRequire.main = { filename: '/factoidal/main.js' };
|
|
389
|
+
|
|
390
|
+
// Faked process. Keeping `versions.node` truthy forces the bundle
|
|
391
|
+
// down its Node branch (which reads argv from process.argv).
|
|
392
|
+
const fakeProc = {
|
|
393
|
+
argv,
|
|
394
|
+
exit: (n) => { exitCode = n | 0; throw EXIT_SENTINEL; },
|
|
395
|
+
stdout: { write: (s) => pushOut(String(s)) },
|
|
396
|
+
stderr: { write: (s) => pushErr(String(s)) },
|
|
397
|
+
platform: 'linux',
|
|
398
|
+
versions: { node: '22.0.0' },
|
|
399
|
+
env: {},
|
|
400
|
+
cpuUsage: () => ({ user: 0, system: 0 }),
|
|
401
|
+
on: () => {},
|
|
402
|
+
cwd: () => '/static',
|
|
403
|
+
chdir: () => {},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
globalThis.process = fakeProc;
|
|
407
|
+
globalThis.require = fakeRequire;
|
|
408
|
+
globalThis.jsoo_fs_tmp = undefined; // wasm bundle doesn't read this, but scrub
|
|
409
|
+
delete globalThis.__fwPromise;
|
|
410
|
+
|
|
411
|
+
function restoreGlobals() {
|
|
412
|
+
globalThis.process = orig.proc;
|
|
413
|
+
globalThis.jsoo_fs_tmp = orig.jsooFs;
|
|
414
|
+
globalThis.fetch = orig.fetch;
|
|
415
|
+
if (orig.require === undefined) delete globalThis.require;
|
|
416
|
+
else globalThis.require = orig.require;
|
|
417
|
+
if (orig.fwPromise === undefined) delete globalThis.__fwPromise;
|
|
418
|
+
else globalThis.__fwPromise = orig.fwPromise;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
// Execute the (rewritten) bundle source. This is synchronous —
|
|
423
|
+
// it spawns an async Promise and stashes it on
|
|
424
|
+
// `globalThis.__fwPromise` via our injected rewrite. We then
|
|
425
|
+
// await that Promise to let wasm instantiation + the CLI body
|
|
426
|
+
// actually run.
|
|
427
|
+
(new Function(src))();
|
|
428
|
+
const p = globalThis.__fwPromise;
|
|
429
|
+
if (!p || typeof p.then !== 'function') {
|
|
430
|
+
throw new Error(
|
|
431
|
+
'browser-wasm: __fwPromise was not captured. ' +
|
|
432
|
+
'The bundle shape may have changed.'
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
await p;
|
|
437
|
+
} catch (e) {
|
|
438
|
+
// The CLI typically calls process.exit(0) on success — our
|
|
439
|
+
// exit() throws EXIT_SENTINEL, which propagates up through
|
|
440
|
+
// the async IIFE. That's the normal path; anything else is
|
|
441
|
+
// a real failure.
|
|
442
|
+
if (e !== EXIT_SENTINEL && !(e && e.__factoidalExit)) {
|
|
443
|
+
throw e;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} finally {
|
|
447
|
+
restoreGlobals();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const stdout = stdoutBuf.join('');
|
|
451
|
+
const stderr = stderrBuf.join('');
|
|
452
|
+
|
|
453
|
+
if (exitCode !== 0) {
|
|
454
|
+
const msg = (stderr || stdout ||
|
|
455
|
+
`factoidal exited with code ${exitCode}`).trim();
|
|
456
|
+
const err = new Error('SPARQL query failed: ' + msg);
|
|
457
|
+
err.exitCode = exitCode;
|
|
458
|
+
err.stderr = stderr;
|
|
459
|
+
err.stdout = stdout;
|
|
460
|
+
throw err;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (output !== 'json') return stdout;
|
|
464
|
+
|
|
465
|
+
const firstBrace = stdout.indexOf('{');
|
|
466
|
+
const lastBrace = stdout.lastIndexOf('}');
|
|
467
|
+
if (firstBrace < 0 || lastBrace < firstBrace) {
|
|
468
|
+
const err = new Error(
|
|
469
|
+
'factoidal (wasm) did not produce JSON on stdout. Raw output: ' + stdout
|
|
470
|
+
);
|
|
471
|
+
err.stdout = stdout;
|
|
472
|
+
err.stderr = stderr;
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
const jsonText = stdout.slice(firstBrace, lastBrace + 1);
|
|
476
|
+
try {
|
|
477
|
+
return JSON.parse(jsonText);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
const err = new Error(
|
|
480
|
+
'factoidal (wasm) JSON parse failed: ' + e.message +
|
|
481
|
+
'. Raw output: ' + stdout
|
|
482
|
+
);
|
|
483
|
+
err.stdout = stdout;
|
|
484
|
+
err.stderr = stderr;
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------
|
|
490
|
+
// npm-entry ABI loader (wasm flavor), browser-driven. Mirrors
|
|
491
|
+
// browser.js's loadNpmEntry() for the js_of_ocaml npm-entry bundle
|
|
492
|
+
// (factoidal-npm-entry.js), but factoidal-npm-entry.wasm.js is
|
|
493
|
+
// wasm_of_ocaml's async-IIFE shape -- same as factoidal.wasm.js above
|
|
494
|
+
// -- rather than a synchronous eval that registers
|
|
495
|
+
// globalThis.factoidalNpmEntry directly. So this loader reuses the
|
|
496
|
+
// same Node-branch-forcing + __fwPromise-capture technique query()
|
|
497
|
+
// above uses to drive the CLI bundle (see that function's header
|
|
498
|
+
// comment for the full rationale), swapped to await the persistent
|
|
499
|
+
// ABI object instead of one call's stdout/stderr. Also mirrors
|
|
500
|
+
// wasm.js's Node-side loadEntry() (npm/factoidal/wasm.js) -- fetch()
|
|
501
|
+
// instead of fs.readFileSync for the source and the .wasm asset, so
|
|
502
|
+
// this works in a real browser with no Node require() at all.
|
|
503
|
+
//
|
|
504
|
+
// Before this loader existed, browser-wasm.js only drove the CLI
|
|
505
|
+
// bundle -- openCottas/queryCottas/closeCottas/toCottas (and every
|
|
506
|
+
// other npm-entry-only export: SPARQL UPDATE, CONSTRUCT, RDFC-1.0
|
|
507
|
+
// canonicalize, SHACL/ShEx/OWL-closure/RML/CSVW/JSON-LD/RIF) were
|
|
508
|
+
// unreachable from the wasm browser entry even though browser.js's js
|
|
509
|
+
// entry already exposed them via loadNpmEntry(). This closes that gap
|
|
510
|
+
// for the in-memory COTTAS bytes store (docs/designissues/2026-07-06-
|
|
511
|
+
// inmemory-bytes-store.md) specifically -- the functions below mirror
|
|
512
|
+
// browser.js's openCottas/queryCottas/closeCottas/toCottas verbatim,
|
|
513
|
+
// against the wasm ABI object instead of the js one.
|
|
514
|
+
// ---------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
const DEFAULT_NPM_ENTRY_WASM_URL =
|
|
517
|
+
new URL('./factoidal-npm-entry.wasm.js', import.meta.url).href;
|
|
518
|
+
|
|
519
|
+
let _npmEntryWasmUrl = DEFAULT_NPM_ENTRY_WASM_URL;
|
|
520
|
+
let _npmEntryWasmSrc = null; // cached (already-rewritten) source
|
|
521
|
+
let _npmEntryFetchPromise = null;
|
|
522
|
+
let _npmEntryNodeFsFallback = null; // test-only: (assetSubPath) => Buffer|Uint8Array
|
|
523
|
+
let _npmEntryWasmAbiPromise = null; // cached loadNpmEntryWasm() result
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Override where `factoidal-npm-entry.wasm.js` is loaded from. The
|
|
527
|
+
* `.wasm` asset in `factoidal-npm-entry.wasm.assets/` is resolved
|
|
528
|
+
* relative to this URL. Mirrors setFactoidalWasmUrl() above.
|
|
529
|
+
*/
|
|
530
|
+
export function setFactoidalNpmEntryWasmUrl(url) {
|
|
531
|
+
_npmEntryWasmUrl = url;
|
|
532
|
+
_npmEntryWasmSrc = null;
|
|
533
|
+
_npmEntryFetchPromise = null;
|
|
534
|
+
_npmEntryWasmAbiPromise = null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Current `factoidal-npm-entry.wasm.js` source URL. */
|
|
538
|
+
export function getFactoidalNpmEntryWasmUrl() {
|
|
539
|
+
return _npmEntryWasmUrl;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Test-only hook. Lets a Node-side test inject an fs-based reader for
|
|
544
|
+
* the npm-entry `.wasm` asset, mirroring `_setWasmAssetFallback()`
|
|
545
|
+
* above for the CLI bundle -- so tests don't have to polyfill fetch()
|
|
546
|
+
* across Node versions.
|
|
547
|
+
* _setNpmEntryWasmAssetFallback((assetSubPath) => fs.readFileSync(...))
|
|
548
|
+
*/
|
|
549
|
+
export function _setNpmEntryWasmAssetFallback(reader) {
|
|
550
|
+
_npmEntryNodeFsFallback = reader;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Test-only hook to seed the cached (already-rewritten) npm-entry wasm
|
|
555
|
+
* source without going through fetch(), mirroring
|
|
556
|
+
* `_setFactoidalWasmSource()` above.
|
|
557
|
+
*/
|
|
558
|
+
export function _setFactoidalNpmEntryWasmSource(src) {
|
|
559
|
+
_npmEntryWasmSrc = rewriteBundle(src);
|
|
560
|
+
_npmEntryWasmAbiPromise = null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function loadNpmEntryWasmSource() {
|
|
564
|
+
if (_npmEntryWasmSrc) return Promise.resolve(_npmEntryWasmSrc);
|
|
565
|
+
if (_npmEntryFetchPromise) return _npmEntryFetchPromise;
|
|
566
|
+
_npmEntryFetchPromise = fetch(_npmEntryWasmUrl)
|
|
567
|
+
.then((r) => {
|
|
568
|
+
if (!r.ok) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`factoidal-npm-entry.wasm.js fetch failed: ${r.status} ${r.statusText}`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
return r.text();
|
|
574
|
+
})
|
|
575
|
+
.then((text) => { _npmEntryWasmSrc = rewriteBundle(text); return _npmEntryWasmSrc; });
|
|
576
|
+
return _npmEntryFetchPromise;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function npmEntryAssetSubPath(urlLike) {
|
|
580
|
+
const s = typeof urlLike === 'string'
|
|
581
|
+
? urlLike
|
|
582
|
+
: (urlLike && urlLike.url) || String(urlLike);
|
|
583
|
+
const marker = 'factoidal-npm-entry.wasm.assets/';
|
|
584
|
+
const ix = s.lastIndexOf(marker);
|
|
585
|
+
return ix < 0 ? s : s.slice(ix);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Fetch + evaluate `factoidal-npm-entry.wasm.js` exactly once,
|
|
590
|
+
* returning the `factoidalNpmEntry` ABI object it registers on
|
|
591
|
+
* globalThis -- the browser-driven counterpart to wasm.js's Node-side
|
|
592
|
+
* `loadEntry()` (npm/factoidal/wasm.js), and the wasm sibling of
|
|
593
|
+
* `loadNpmEntry()` above. Cached: subsequent calls return the same
|
|
594
|
+
* resolved ABI object without re-fetching or re-evaluating the bundle.
|
|
595
|
+
*
|
|
596
|
+
* @returns {Promise<object>} the factoidalNpmEntry ABI object.
|
|
597
|
+
*/
|
|
598
|
+
export async function loadNpmEntryWasm() {
|
|
599
|
+
if (_npmEntryWasmAbiPromise) return _npmEntryWasmAbiPromise;
|
|
600
|
+
_npmEntryWasmAbiPromise = (async () => {
|
|
601
|
+
const src = await loadNpmEntryWasmSource();
|
|
602
|
+
|
|
603
|
+
const orig = {
|
|
604
|
+
proc: globalThis.process,
|
|
605
|
+
require: globalThis.require,
|
|
606
|
+
fwPromise: globalThis.__fwPromise,
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Resolve the .wasm asset: prefer the test-injected reader, else
|
|
610
|
+
// fetch() relative to _npmEntryWasmUrl (same shape as query()'s
|
|
611
|
+
// readWasmAsset above, against the npm-entry bundle's own URL).
|
|
612
|
+
async function readWasmAsset(requestedPath) {
|
|
613
|
+
const sub = npmEntryAssetSubPath(String(requestedPath));
|
|
614
|
+
if (_npmEntryNodeFsFallback) {
|
|
615
|
+
const bytes = await _npmEntryNodeFsFallback(sub);
|
|
616
|
+
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
617
|
+
}
|
|
618
|
+
const resolved = new URL(sub, _npmEntryWasmUrl).href;
|
|
619
|
+
const fetchFn = globalThis.fetch;
|
|
620
|
+
if (!fetchFn) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
'loadNpmEntryWasm: no fetch() available and no ' +
|
|
623
|
+
'_setNpmEntryWasmAssetFallback() injected. In Node <20 or an ' +
|
|
624
|
+
'exotic runtime, set a fallback reader.'
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
const r = await fetchFn(resolved);
|
|
628
|
+
if (!r.ok) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
`npm-entry wasm asset fetch failed: ${r.status} ${r.statusText} (${resolved})`);
|
|
631
|
+
}
|
|
632
|
+
return new Uint8Array(await r.arrayBuffer());
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// No CLI data files here -- the entry ABI is a pure
|
|
636
|
+
// string-in/JSON-out interface (parseToDatasetJson/queryDataset/
|
|
637
|
+
// openCottas/... -- see bin/npm-entry/entry_jsoo.ml's file
|
|
638
|
+
// header). `node:fs` is still `require()`d unconditionally by the
|
|
639
|
+
// wasm_of_ocaml runtime whenever it detects the Node branch
|
|
640
|
+
// (`f=g&&require("node:fs")` at the top of the bundle) even
|
|
641
|
+
// though the entry ABI itself never touches a real file --
|
|
642
|
+
// provide an always-ENOENT stub so that unconditional require()
|
|
643
|
+
// doesn't throw "require is not defined" in a real browser (which
|
|
644
|
+
// has no `node:fs` at all).
|
|
645
|
+
// The wasm_of_ocaml runtime's channel writer calls fs.writeSync(fd, ...)
|
|
646
|
+
// directly for stdout/stderr (fd 1/2) rather than process.stdout.write
|
|
647
|
+
// -- the entry ABI is otherwise silent (it returns JSON strings, it
|
|
648
|
+
// doesn't print), but any incidental Printf/logging still routes
|
|
649
|
+
// through here, so writeSync must be a real (if discarding) function
|
|
650
|
+
// rather than absent, or the runtime throws
|
|
651
|
+
// "f.writeSync is not a function" the first time anything writes.
|
|
652
|
+
const fakeFs = {
|
|
653
|
+
constants: {
|
|
654
|
+
R_OK: 4, W_OK: 2, X_OK: 1, F_OK: 0,
|
|
655
|
+
O_RDONLY: 0, O_WRONLY: 1, O_RDWR: 2, O_APPEND: 8, O_CREAT: 512,
|
|
656
|
+
O_TRUNC: 1024, O_EXCL: 2048, O_NONBLOCK: 4096, O_NOCTTY: 8192,
|
|
657
|
+
O_DSYNC: 4194304, O_SYNC: 128,
|
|
658
|
+
},
|
|
659
|
+
writeSync: (fd, buf, offset, length) => {
|
|
660
|
+
return length === undefined
|
|
661
|
+
? (typeof buf === 'string' ? buf.length : buf.byteLength)
|
|
662
|
+
: length;
|
|
663
|
+
},
|
|
664
|
+
openSync: (p) => {
|
|
665
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${p}'`),
|
|
666
|
+
{ code: 'ENOENT', errno: -2, syscall: 'open', path: p });
|
|
667
|
+
},
|
|
668
|
+
closeSync: () => {},
|
|
669
|
+
readSync: () => 0,
|
|
670
|
+
fsyncSync: () => {},
|
|
671
|
+
existsSync: () => false,
|
|
672
|
+
accessSync: (p) => {
|
|
673
|
+
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT', errno: -2, path: p });
|
|
674
|
+
},
|
|
675
|
+
statSync: (p, o) => {
|
|
676
|
+
if (o && o.throwIfNoEntry === false) return undefined;
|
|
677
|
+
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT', errno: -2, path: p });
|
|
678
|
+
},
|
|
679
|
+
lstatSync(p, o) { return fakeFs.statSync(p, o); },
|
|
680
|
+
fstatSync: (fd, o) => makeStat(0, false, o),
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const fakeRequire = (name) => {
|
|
684
|
+
if (name === 'node:fs') return fakeFs;
|
|
685
|
+
if (name === 'node:fs/promises') return { readFile: readWasmAsset };
|
|
686
|
+
if (name === 'node:path') return {
|
|
687
|
+
join: (...parts) => parts.filter(Boolean).join('/').replace(/\/+/g, '/'),
|
|
688
|
+
dirname: (p) => String(p).replace(/\/[^/]*$/, '') || '.',
|
|
689
|
+
};
|
|
690
|
+
if (name === 'node:os') return { tmpdir: () => '/tmp' };
|
|
691
|
+
if (name === 'node:tty') return { isatty: () => false };
|
|
692
|
+
if (name === 'node:child_process') return { spawnSync: () => ({ status: 0, signal: null }) };
|
|
693
|
+
throw new Error(`browser-wasm npm-entry shim: unexpected require('${name}')`);
|
|
694
|
+
};
|
|
695
|
+
// Same trick as query()'s fakeRequire.main below: the bundle joins
|
|
696
|
+
// dirname(require.main.filename) onto its relative asset path,
|
|
697
|
+
// which we then strip back to the subpath inside readWasmAsset.
|
|
698
|
+
fakeRequire.main = { filename: '/factoidal/npm-entry.js' };
|
|
699
|
+
|
|
700
|
+
// Faked process -- forces the bundle's Node branch (real I/O via
|
|
701
|
+
// require()) rather than its browser branch (fixed argv, no
|
|
702
|
+
// persistent-ABI registration call). No argv-driven CLI runs
|
|
703
|
+
// through this path, so stdout/stderr/exit are inert stubs.
|
|
704
|
+
const fakeProc = {
|
|
705
|
+
argv: ['node', 'factoidal-npm-entry'],
|
|
706
|
+
exit: () => {},
|
|
707
|
+
stdout: { write: () => true },
|
|
708
|
+
stderr: { write: () => true },
|
|
709
|
+
platform: 'linux',
|
|
710
|
+
versions: { node: '22.0.0' },
|
|
711
|
+
env: {},
|
|
712
|
+
cpuUsage: () => ({ user: 0, system: 0 }),
|
|
713
|
+
on: () => {},
|
|
714
|
+
cwd: () => '/static',
|
|
715
|
+
chdir: () => {},
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
globalThis.process = fakeProc;
|
|
719
|
+
globalThis.require = fakeRequire;
|
|
720
|
+
delete globalThis.__fwPromise;
|
|
721
|
+
|
|
722
|
+
function restoreGlobals() {
|
|
723
|
+
globalThis.process = orig.proc;
|
|
724
|
+
if (orig.require === undefined) delete globalThis.require;
|
|
725
|
+
else globalThis.require = orig.require;
|
|
726
|
+
if (orig.fwPromise === undefined) delete globalThis.__fwPromise;
|
|
727
|
+
else globalThis.__fwPromise = orig.fwPromise;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
(new Function(src))();
|
|
732
|
+
const p = globalThis.__fwPromise;
|
|
733
|
+
if (!p || typeof p.then !== 'function') {
|
|
734
|
+
throw new Error(
|
|
735
|
+
'loadNpmEntryWasm: __fwPromise was not captured. ' +
|
|
736
|
+
'The bundle shape may have changed.'
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
await p;
|
|
740
|
+
} finally {
|
|
741
|
+
restoreGlobals();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const abi = globalThis.factoidalNpmEntry;
|
|
745
|
+
if (!abi || typeof abi.queryDataset !== 'function') {
|
|
746
|
+
throw new Error(
|
|
747
|
+
'loadNpmEntryWasm: factoidal-npm-entry.wasm.js loaded but did not ' +
|
|
748
|
+
'register a usable factoidalNpmEntry on globalThis'
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
return abi;
|
|
752
|
+
})();
|
|
753
|
+
return _npmEntryWasmAbiPromise;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Open a COTTAS/Parquet artifact's raw bytes as a queryable, read-only
|
|
758
|
+
* store, on the wasm engine (bin/npm-entry/entry_jsoo.ml's openCottas
|
|
759
|
+
* export, compiled through wasm_of_ocaml). Wasm sibling of browser.js's
|
|
760
|
+
* openCottas() -- see that function's doc comment for the full design
|
|
761
|
+
* rationale (lazy row decode, no heap Dataset materialization).
|
|
762
|
+
*
|
|
763
|
+
* @param {string|Uint8Array|ArrayBuffer} bytes whole `.cottas` file contents
|
|
764
|
+
* @returns {Promise<string>} opaque handle for queryCottas()/closeCottas()
|
|
765
|
+
*/
|
|
766
|
+
export async function openCottas(bytes) {
|
|
767
|
+
let hex;
|
|
768
|
+
if (typeof bytes === 'string') {
|
|
769
|
+
if (!/^[0-9a-fA-F]*$/.test(bytes) || bytes.length % 2 !== 0) {
|
|
770
|
+
throw new TypeError('openCottas: string input must be an even-length hex string');
|
|
771
|
+
}
|
|
772
|
+
hex = bytes.toLowerCase();
|
|
773
|
+
} else {
|
|
774
|
+
const u8 = bytes instanceof Uint8Array ? bytes
|
|
775
|
+
: bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : null;
|
|
776
|
+
if (!u8) {
|
|
777
|
+
throw new TypeError('openCottas: expected a hex string, Uint8Array, or ArrayBuffer');
|
|
778
|
+
}
|
|
779
|
+
const HEX = '0123456789abcdef';
|
|
780
|
+
const parts = new Array(u8.length);
|
|
781
|
+
for (let i = 0; i < u8.length; i++) {
|
|
782
|
+
parts[i] = HEX[u8[i] >> 4] + HEX[u8[i] & 15];
|
|
783
|
+
}
|
|
784
|
+
hex = parts.join('');
|
|
785
|
+
}
|
|
786
|
+
const abi = await loadNpmEntryWasm();
|
|
787
|
+
if (typeof abi.openCottas !== 'function') {
|
|
788
|
+
throw new Error('openCottas: the loaded factoidal-npm-entry.wasm.js bundle predates the openCottas export');
|
|
789
|
+
}
|
|
790
|
+
const parsed = JSON.parse(abi.openCottas(hex));
|
|
791
|
+
if (!parsed.ok) throw new Error(parsed.error || 'openCottas failed');
|
|
792
|
+
return parsed.handle;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Run a SPARQL 1.1 query against a store opened by openCottas(), on
|
|
797
|
+
* the wasm engine (bin/npm-entry/entry_jsoo.ml's queryCottas export).
|
|
798
|
+
* No `entail` option and no write overlay (read-only) -- see
|
|
799
|
+
* browser.js's queryCottas() doc comment for the full divergence list
|
|
800
|
+
* from query().
|
|
801
|
+
*
|
|
802
|
+
* @param {string} handle from openCottas()
|
|
803
|
+
* @param {string} sparql
|
|
804
|
+
* @returns {Promise<{ok:true,kind:'select',srj:object}|{ok:true,kind:'ask',boolean:boolean}|{ok:true,kind:'construct',nquads:string}>}
|
|
805
|
+
*/
|
|
806
|
+
export async function queryCottas(handle, sparql) {
|
|
807
|
+
if (typeof handle !== 'string') {
|
|
808
|
+
throw new TypeError('queryCottas: handle must be the string openCottas() returned');
|
|
809
|
+
}
|
|
810
|
+
if (typeof sparql !== 'string') {
|
|
811
|
+
throw new TypeError('queryCottas: sparql must be a string');
|
|
812
|
+
}
|
|
813
|
+
const abi = await loadNpmEntryWasm();
|
|
814
|
+
if (typeof abi.queryCottas !== 'function') {
|
|
815
|
+
throw new Error('queryCottas: the loaded factoidal-npm-entry.wasm.js bundle predates the queryCottas export');
|
|
816
|
+
}
|
|
817
|
+
const parsed = JSON.parse(abi.queryCottas(handle, sparql));
|
|
818
|
+
if (!parsed.ok) throw new Error(parsed.error || 'queryCottas failed');
|
|
819
|
+
return parsed;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Release a store opened by openCottas(), on the wasm engine
|
|
824
|
+
* (bin/npm-entry/entry_jsoo.ml's closeCottas export). Drops the
|
|
825
|
+
* handle from the entry bundle's own registry only -- does not evict
|
|
826
|
+
* the underlying byte cache.
|
|
827
|
+
* @param {string} handle
|
|
828
|
+
* @returns {Promise<void>}
|
|
829
|
+
*/
|
|
830
|
+
export async function closeCottas(handle) {
|
|
831
|
+
const abi = await loadNpmEntryWasm();
|
|
832
|
+
if (typeof abi.closeCottas !== 'function') {
|
|
833
|
+
throw new Error('closeCottas: the loaded factoidal-npm-entry.wasm.js bundle predates the closeCottas export');
|
|
834
|
+
}
|
|
835
|
+
const parsed = JSON.parse(abi.closeCottas(handle));
|
|
836
|
+
if (!parsed.ok) throw new Error(parsed.error || 'closeCottas failed');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Serialize a dataset-handle N-Quads string to COTTAS/Parquet bytes,
|
|
841
|
+
* on the wasm engine (bin/npm-entry/entry_jsoo.ml's toCottas export;
|
|
842
|
+
* RDF.CottasStore.BaseWriter.serialize_cottas_v2, the same pure `Tot`
|
|
843
|
+
* function `factoidal compact --native-writer` uses). Round-trips
|
|
844
|
+
* into openCottas() byte-for-byte, on either engine (see
|
|
845
|
+
* cottas-bytes-store-wasm.test.js's js/wasm parity test).
|
|
846
|
+
*
|
|
847
|
+
* @param {string} nQuads dataset-handle N-Quads text
|
|
848
|
+
* @returns {Promise<Uint8Array>}
|
|
849
|
+
*/
|
|
850
|
+
export async function toCottas(nQuads) {
|
|
851
|
+
if (typeof nQuads !== 'string') {
|
|
852
|
+
throw new TypeError('toCottas: nQuads must be a string');
|
|
853
|
+
}
|
|
854
|
+
const abi = await loadNpmEntryWasm();
|
|
855
|
+
if (typeof abi.toCottas !== 'function') {
|
|
856
|
+
throw new Error('toCottas: the loaded factoidal-npm-entry.wasm.js bundle predates the toCottas export');
|
|
857
|
+
}
|
|
858
|
+
const parsed = JSON.parse(abi.toCottas(nQuads));
|
|
859
|
+
if (!parsed.ok) throw new Error(parsed.error || 'toCottas failed');
|
|
860
|
+
const hex = parsed.cottasHex;
|
|
861
|
+
const out = new Uint8Array(hex.length / 2);
|
|
862
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
863
|
+
return out;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export const version = '0.1.0';
|
|
867
|
+
|
|
868
|
+
export default {
|
|
869
|
+
query, version, setFactoidalWasmUrl, getFactoidalWasmUrl,
|
|
870
|
+
loadNpmEntryWasm, setFactoidalNpmEntryWasmUrl, getFactoidalNpmEntryWasmUrl,
|
|
871
|
+
openCottas, queryCottas, closeCottas, toCottas,
|
|
872
|
+
};
|