@factoidal/core 0.7.1 → 0.10.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 +503 -1
- package/NOTICE +2 -21
- package/README.md +117 -993
- package/api.d.ts +53 -0
- package/api.js +5 -0
- package/api.mjs +56 -0
- package/bin/engine.mjs +38 -19
- package/bin/factoidal.mjs +512 -66
- package/bin/pack.mjs +37 -0
- package/bin/serve-worker.mjs +32 -0
- package/bin/serve.mjs +423 -0
- package/bin/store-worker-host.mjs +1 -1
- package/bin/store.mjs +1 -1
- package/browser.d.ts +110 -507
- package/browser.js +151 -2257
- package/docs/fuseki-replacement.md +108 -0
- package/fn.d.ts +24 -95
- package/fn.js +104 -150
- package/index.d.ts +598 -259
- package/index.js +4 -234
- package/index.mjs +176 -84
- package/l4-assets/l4factoidal.js +47 -7
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +11 -7
- package/lib/api.mjs +2300 -0
- package/lib/browser-engine.mjs +69 -0
- package/lib/dataset.mjs +377 -0
- package/lib/delta-log-hosts.mjs +191 -0
- package/lib/lean-entry.mjs +204 -0
- package/lib/pure.mjs +98 -0
- package/lib/store.mjs +115 -0
- package/lib/version.mjs +5 -0
- package/lws/README.md +97 -0
- package/lws/server.mjs +331 -0
- package/package.json +59 -61
- package/rdfjs.js +5 -536
- package/rdfjs.mjs +594 -0
- package/sample-store.d.ts +8 -16
- package/sample-store.mjs +39 -29
- package/solid/client/README.md +100 -0
- package/solid/client/index.mjs +277 -0
- package/solid/server/README.md +96 -0
- package/solid/server/index.mjs +317 -0
- package/store-host/deno.mjs +8 -0
- package/store-host/index.mjs +12 -0
- package/store-host/node.mjs +8 -0
- package/xmpp/README.md +107 -0
- package/xmpp/index.mjs +229 -0
- package/browser-wasm.js +0 -872
- package/factoidal-npm-entry.js +0 -15477
- package/factoidal-npm-entry.wasm.assets/code-5a7fc68f2ab1323718b8.wasm +0 -0
- package/factoidal-npm-entry.wasm.js +0 -455
- package/factoidal.js +0 -13118
- package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
- package/factoidal.wasm.js +0 -457
- package/hacl-init.js +0 -92
- 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 +0 -775
- package/hacl-wasm/api.json +0 -3787
- package/hacl-wasm/layouts.json +0 -1
- package/hacl-wasm/loader.js +0 -568
- package/hacl-wasm/shell.js +0 -12
- package/l4-core.d.ts +0 -159
- package/l4-core.js +0 -188
- package/l4.d.ts +0 -50
- package/l4.js +0 -104
- package/lib/api.js +0 -2300
- package/lib/engine-js.js +0 -165
- package/lib/engine-wasm.js +0 -300
- package/sample-store/CURRENT +0 -1
- package/sample-store/gen-1/manifest.sbm2 +0 -0
- package/sample-store/gen-1/manifest.tsv +0 -14
- package/sample-store/gen-1/predicate-0.ibk3 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-3.ibk3 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +0 -2
- package/sample-store/gen-1/predicate-5.ibk3 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +0 -2
- package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +0 -3
- package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +0 -2
- package/select.d.ts +0 -116
- package/select.js +0 -492
- package/version.json +0 -102
- package/wasm.d.ts +0 -130
- package/wasm.js +0 -158
package/bin/pack.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// and writes each artifact as it appears, so neither side ever holds the
|
|
24
24
|
// whole input or the whole generation.
|
|
25
25
|
|
|
26
|
+
import { appendFileSync } from 'node:fs'
|
|
26
27
|
import { StoreHostError, readChunk, writeNew } from '../store-host/index.mjs'
|
|
27
28
|
import { joinPath } from '../store-host/paths.mjs'
|
|
28
29
|
|
|
@@ -31,6 +32,40 @@ import { joinPath } from '../store-host/paths.mjs'
|
|
|
31
32
|
* what makes the byte-identity gate meaningful. */
|
|
32
33
|
export const FEED_BYTES = 65536
|
|
33
34
|
|
|
35
|
+
/** A diagnostic trace of the module's linear memory, one line per feed
|
|
36
|
+
* and one per drain, written when `L4_PACK_MEM` names a file. It exists
|
|
37
|
+
* because linear memory only ever grows, so a memory question about a
|
|
38
|
+
* pack needs the curve per feed rather than one figure per process. The
|
|
39
|
+
* worker thread's `process.stderr` does not flush while the pack loop
|
|
40
|
+
* runs, so the trace is written synchronously to a file.
|
|
41
|
+
* Design record: docs/designissues/2026-09-07-wasm-packer-memory.md. */
|
|
42
|
+
function memNote (engine, line) {
|
|
43
|
+
const path = process.env.L4_PACK_MEM
|
|
44
|
+
if (!path) return
|
|
45
|
+
const m = engine._module
|
|
46
|
+
const heap = m ? m.HEAPU8.byteLength : -1
|
|
47
|
+
appendFileSync(path, `${line} ${heap}${m ? allocatorNote(m) : ''}\n`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** mimalloc's own totals, appended to a trace line when the module
|
|
51
|
+
* exports them. `heap` alone cannot say whether the packer references
|
|
52
|
+
* the bytes or the allocator holds them free; `reserved`, `committed`
|
|
53
|
+
* and the requested-malloc total do. The counters read zero unless the
|
|
54
|
+
* module was built with `L4_WASM_MIMALLOC_CFLAGS=-DMI_STAT=2`. */
|
|
55
|
+
function allocatorNote (m) {
|
|
56
|
+
if (typeof m._l4_mem_stats_c !== 'function') return ''
|
|
57
|
+
const ptr = m._l4_mem_stats_c()
|
|
58
|
+
if (!ptr) return ''
|
|
59
|
+
let text
|
|
60
|
+
try { text = m.UTF8ToString(ptr) } finally { m._l4_free_result(ptr) }
|
|
61
|
+
const field = (name) => {
|
|
62
|
+
const row = text.split('\n').find((l) => l.trim().startsWith(name + ':'))
|
|
63
|
+
return row ? row.trim().replace(/\s+/g, ' ') : ''
|
|
64
|
+
}
|
|
65
|
+
return ' | ' + ['reserved', 'committed', 'touched', 'malloc']
|
|
66
|
+
.map(field).filter(Boolean).join(' | ')
|
|
67
|
+
}
|
|
68
|
+
|
|
34
69
|
/** An error the pack operations reported, with the engine's own words. */
|
|
35
70
|
export class PackError extends Error {
|
|
36
71
|
constructor (message, detail = {}) {
|
|
@@ -130,7 +165,9 @@ export function packFile (engine, input, output, options) {
|
|
|
130
165
|
offset += chunk.length
|
|
131
166
|
bytesRead += chunk.length
|
|
132
167
|
engine.callBlobIO('packFeed', [handle], chunk)
|
|
168
|
+
memNote(engine, `feed ${passName} ${offset}`)
|
|
133
169
|
const drained = drain(engine, handle, output)
|
|
170
|
+
memNote(engine, `drain ${passName} ${offset}`)
|
|
134
171
|
written.push(...drained.names)
|
|
135
172
|
bytesWritten += drained.bytes
|
|
136
173
|
if (typeof options.onProgress === 'function') {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The worker thread of `factoidal serve` (`listenOnWorker` in ./serve.mjs):
|
|
2
|
+
// the engine, the served store and the sockets live here, on a thread whose
|
|
3
|
+
// call stack the parent raised. This file starts `listen`, reports where it
|
|
4
|
+
// is listening, and closes on request. It decides nothing else.
|
|
5
|
+
import { parentPort, workerData } from 'node:worker_threads'
|
|
6
|
+
import { listen } from './serve.mjs'
|
|
7
|
+
|
|
8
|
+
let running = null
|
|
9
|
+
try {
|
|
10
|
+
running = await listen(workerData.root, {
|
|
11
|
+
...workerData.options,
|
|
12
|
+
onError: (error) => {
|
|
13
|
+
console.error(`factoidal serve: ${error && error.message ? error.message : error}`)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
parentPort.postMessage({
|
|
17
|
+
kind: 'listening', origin: running.origin, port: running.port, info: running.served.info
|
|
18
|
+
})
|
|
19
|
+
} catch (error) {
|
|
20
|
+
parentPort.postMessage({
|
|
21
|
+
kind: 'error',
|
|
22
|
+
message: error && error.message ? error.message : String(error),
|
|
23
|
+
unknownOp: error && error.unknownOp === true
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
parentPort.on('message', async (message) => {
|
|
28
|
+
if (message && message.kind === 'close') {
|
|
29
|
+
if (running !== null) await running.close()
|
|
30
|
+
parentPort.postMessage({ kind: 'closed' })
|
|
31
|
+
}
|
|
32
|
+
})
|
package/bin/serve.mjs
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// `factoidal serve`: a SPARQL 1.1 Protocol, Graph Store Protocol and Service
|
|
2
|
+
// Description endpoint over one activated Shardborough collection.
|
|
3
|
+
// https://github.com/danbri/factoidal/issues/669
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE IS ALLOWED TO DO
|
|
6
|
+
// Accept TCP connections, hand the bytes a connection delivered to the
|
|
7
|
+
// engine's `storeServeRequest`, write the bytes the engine answers, read the
|
|
8
|
+
// store files the engine names, and append the bytes the engine hands back
|
|
9
|
+
// to the file it names, at the size it names. That is the whole contract.
|
|
10
|
+
//
|
|
11
|
+
// It makes NO HTTP and NO SPARQL decision. It does not know where a request
|
|
12
|
+
// ends (Content-Length and chunked framing are in
|
|
13
|
+
// `formal/lean4/L4Factoidal/HTTP/Wire.lean`), what a header means, which
|
|
14
|
+
// endpoint a path is, which status a request earns, which media type an
|
|
15
|
+
// answer takes, or what an update changes. All of that is in
|
|
16
|
+
// `formal/lean4/L4Factoidal/SPARQL/StoreEndpoint.lean` and the protocol
|
|
17
|
+
// modules it calls (iron rule 7 of CLAUDE.md). It reads no manifest field,
|
|
18
|
+
// decodes no block and computes no digest: the engine names the files to
|
|
19
|
+
// read (`storeServeKeys`) and verifies them (`storeServeOpen`).
|
|
20
|
+
//
|
|
21
|
+
// The one status this file writes itself is 500, and only when the engine
|
|
22
|
+
// call throws or the host cannot write: a failure of this host.
|
|
23
|
+
//
|
|
24
|
+
// THE WASM CONTRACT (`formal/lean4/Wasm/Ops/StoreServe.lean`)
|
|
25
|
+
// storeServeKeys [manifestHex] -> { keys, state }
|
|
26
|
+
// storeServeOpen [manifestHex, artifactsJson, stateJson, configJson]
|
|
27
|
+
// + region -> { handle, ... }
|
|
28
|
+
// storeServeRequest [handle] + the connection's bytes
|
|
29
|
+
// -> { state: 'incomplete', need? }
|
|
30
|
+
// | { state: 'respond', consumed, close, status } + response bytes
|
|
31
|
+
// | { state: 'append', consumed, close, pending, key, expectedSize }
|
|
32
|
+
// + the bytes to append
|
|
33
|
+
// storeServeCommit [handle, pending, outcome] -> { state: 'respond',
|
|
34
|
+
// close, status }
|
|
35
|
+
// + response bytes
|
|
36
|
+
// storeServeClose [handle] -> {}
|
|
37
|
+
//
|
|
38
|
+
// An update is two calls. The engine answers `append`; this host appends
|
|
39
|
+
// the bytes to the named file only if the file has exactly `expectedSize`
|
|
40
|
+
// bytes, syncs it, and reports "ok" (or why not) to `storeServeCommit`,
|
|
41
|
+
// which answers the response either way. The append is synchronous, so no
|
|
42
|
+
// other request is served between the two calls.
|
|
43
|
+
//
|
|
44
|
+
// Concurrency: one engine instance, single-threaded; connections are served
|
|
45
|
+
// one engine call at a time. Several processes must not serve one
|
|
46
|
+
// collection for writing: the append has no advisory lock
|
|
47
|
+
// (https://github.com/danbri/factoidal/issues/644).
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
StoreHostError, appendSyncAtSize, listNames, openCollection, readWhole
|
|
51
|
+
} from '../store-host/index.mjs'
|
|
52
|
+
import { baseName, joinPath, requireChildName } from '../store-host/paths.mjs'
|
|
53
|
+
import { hexOfBytes, loadEngine } from './engine.mjs'
|
|
54
|
+
|
|
55
|
+
/** The ops this host needs from the engine. */
|
|
56
|
+
export const SERVE_OPS = [
|
|
57
|
+
'storeServeKeys', 'storeServeOpen', 'storeServeRequest', 'storeServeCommit',
|
|
58
|
+
'storeServeClose'
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** The extra ops `serve --tdb2` needs. */
|
|
62
|
+
export const TDB2_SERVE_OPS = ['tdb2ServeKeys', 'tdb2ServeOpen']
|
|
63
|
+
|
|
64
|
+
/** A failure of this host, as opposed to an answer of the engine.
|
|
65
|
+
* `unknownOp` is set when the loaded module does not carry the ops. */
|
|
66
|
+
export class ServeHostError extends Error {
|
|
67
|
+
constructor (message, detail = {}) {
|
|
68
|
+
super(message)
|
|
69
|
+
this.name = 'ServeHostError'
|
|
70
|
+
this.unknownOp = detail.unknownOp === true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isUnknownOp (error) {
|
|
75
|
+
const text = error && error.message ? String(error.message) : String(error)
|
|
76
|
+
return text.indexOf('unknown op') >= 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function concatenate (chunks) {
|
|
80
|
+
let total = 0
|
|
81
|
+
for (const chunk of chunks) total += chunk.length
|
|
82
|
+
const out = new Uint8Array(total)
|
|
83
|
+
let offset = 0
|
|
84
|
+
for (const chunk of chunks) {
|
|
85
|
+
out.set(chunk, offset)
|
|
86
|
+
offset += chunk.length
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Read one file the engine named, or null when it does not exist. */
|
|
92
|
+
function readOptional (path) {
|
|
93
|
+
try {
|
|
94
|
+
return readWhole(path)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof StoreHostError && error.code === 'OPEN_FAILED') return null
|
|
97
|
+
throw error
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Open the activated generation of a collection for serving.
|
|
103
|
+
*
|
|
104
|
+
* Reads CURRENT and the manifest, asks the engine which files it needs,
|
|
105
|
+
* reads them, and hands every byte to `storeServeOpen` in one region.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} root the collection root that holds CURRENT
|
|
108
|
+
* @param {object} [options]
|
|
109
|
+
* @param {object} [options.engine] an engine handle to reuse
|
|
110
|
+
* @param {string} [options.queryPath] the query and update endpoint path
|
|
111
|
+
* @param {string} [options.gspPath] the Graph Store endpoint path
|
|
112
|
+
* @returns {Promise<object>} the served store: `request(bytes)`,
|
|
113
|
+
* `commit(pending, outcome)`, `close()`, `info`, `generationDir`
|
|
114
|
+
*/
|
|
115
|
+
export async function openServedStore (root, options = {}) {
|
|
116
|
+
const engine = options.engine !== undefined ? options.engine : await loadEngine()
|
|
117
|
+
const opened = openCollection(root)
|
|
118
|
+
const manifestHex = hexOfBytes(opened.manifest)
|
|
119
|
+
let named
|
|
120
|
+
try {
|
|
121
|
+
named = engine.call('storeServeKeys', [manifestHex])
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (isUnknownOp(error)) {
|
|
124
|
+
throw new ServeHostError(
|
|
125
|
+
'the loaded l4factoidal WebAssembly module does not carry the storeServe ops ' +
|
|
126
|
+
'(rebuild formal/lean4/Wasm)', { unknownOp: true })
|
|
127
|
+
}
|
|
128
|
+
throw error
|
|
129
|
+
}
|
|
130
|
+
const chunks = []
|
|
131
|
+
const artifacts = []
|
|
132
|
+
const state = []
|
|
133
|
+
let offset = 0
|
|
134
|
+
for (const key of named.keys) {
|
|
135
|
+
const bytes = readWhole(joinPath(opened.generationDir, requireChildName(key, 'artifact key')))
|
|
136
|
+
artifacts.push({ key, offset, len: bytes.length })
|
|
137
|
+
chunks.push(bytes)
|
|
138
|
+
offset += bytes.length
|
|
139
|
+
}
|
|
140
|
+
for (const key of named.state) {
|
|
141
|
+
const bytes = readOptional(joinPath(opened.generationDir, requireChildName(key, 'state key')))
|
|
142
|
+
if (bytes === null) continue
|
|
143
|
+
state.push({ key, offset, len: bytes.length })
|
|
144
|
+
chunks.push(bytes)
|
|
145
|
+
offset += bytes.length
|
|
146
|
+
}
|
|
147
|
+
const config = {}
|
|
148
|
+
if (typeof options.queryPath === 'string') config.queryPath = options.queryPath
|
|
149
|
+
if (typeof options.gspPath === 'string') config.gspPath = options.gspPath
|
|
150
|
+
const info = engine.callBlobIO('storeServeOpen',
|
|
151
|
+
[manifestHex, JSON.stringify(artifacts), JSON.stringify(state), JSON.stringify(config)],
|
|
152
|
+
concatenate(chunks)).envelope
|
|
153
|
+
const handle = info.handle
|
|
154
|
+
return {
|
|
155
|
+
engine,
|
|
156
|
+
info,
|
|
157
|
+
root,
|
|
158
|
+
generation: opened.generation,
|
|
159
|
+
generationDir: opened.generationDir,
|
|
160
|
+
/** One engine step over the bytes a connection delivered so far. */
|
|
161
|
+
request (bytes) {
|
|
162
|
+
const answer = engine.callBlobIO('storeServeRequest', [handle], bytes)
|
|
163
|
+
return { envelope: answer.envelope, bytes: answer.bytes }
|
|
164
|
+
},
|
|
165
|
+
/** Report the outcome of an append the engine asked for. */
|
|
166
|
+
commit (pending, outcome) {
|
|
167
|
+
const answer = engine.callBlobIO('storeServeCommit', [handle, pending, outcome],
|
|
168
|
+
new Uint8Array(0))
|
|
169
|
+
return { envelope: answer.envelope, bytes: answer.bytes }
|
|
170
|
+
},
|
|
171
|
+
/** Perform the append an `append` envelope names; the outcome text. */
|
|
172
|
+
append (envelope, bytes) {
|
|
173
|
+
const path = joinPath(opened.generationDir, requireChildName(envelope.key, 'append key'))
|
|
174
|
+
try {
|
|
175
|
+
return appendSyncAtSize(path, bytes, envelope.expectedSize)
|
|
176
|
+
? 'ok'
|
|
177
|
+
: `${envelope.key} did not have ${envelope.expectedSize} bytes; another writer changed it`
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return error && error.message ? error.message : String(error)
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
close () {
|
|
183
|
+
try { engine.call('storeServeClose', [handle]) } catch (_error) { /* already gone */ }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Open a Jena TDB2 database directory for serving, read only
|
|
190
|
+
* (https://github.com/danbri/factoidal/issues/834).
|
|
191
|
+
*
|
|
192
|
+
* Lists the directory, asks the engine which generation is current and which
|
|
193
|
+
* files it reads (`tdb2ServeKeys`), reads them, and hands every byte to
|
|
194
|
+
* `tdb2ServeOpen` in one region. The engine decodes the B+trees and the node
|
|
195
|
+
* table (`formal/lean4/L4Factoidal/Tdb2/Read.lean`); this host decodes nothing.
|
|
196
|
+
* The returned object has the shape of `openServedStore`'s, and its engine
|
|
197
|
+
* answers every request that would change the dataset with 501, so `append`
|
|
198
|
+
* is never reached.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} root the TDB2 database directory (or one Data-NNNN directory)
|
|
201
|
+
* @param {object} [options] as `openServedStore`
|
|
202
|
+
*/
|
|
203
|
+
export async function openServedTdb2 (root, options = {}) {
|
|
204
|
+
const engine = options.engine !== undefined ? options.engine : await loadEngine()
|
|
205
|
+
let named
|
|
206
|
+
try {
|
|
207
|
+
named = engine.call('tdb2ServeKeys', [baseName(root), JSON.stringify(listNames(root))])
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (isUnknownOp(error)) {
|
|
210
|
+
throw new ServeHostError(
|
|
211
|
+
'the loaded l4factoidal WebAssembly module does not carry the tdb2Serve ops ' +
|
|
212
|
+
'(rebuild formal/lean4/Wasm)', { unknownOp: true })
|
|
213
|
+
}
|
|
214
|
+
throw error
|
|
215
|
+
}
|
|
216
|
+
const generationDir = named.generation === '' ? root : joinPath(root, requireChildName(named.generation, 'generation'))
|
|
217
|
+
const chunks = []
|
|
218
|
+
const files = []
|
|
219
|
+
let offset = 0
|
|
220
|
+
for (const key of named.keys) {
|
|
221
|
+
const bytes = readWhole(joinPath(generationDir, requireChildName(key, 'TDB2 file')))
|
|
222
|
+
files.push({ key, offset, len: bytes.length })
|
|
223
|
+
chunks.push(bytes)
|
|
224
|
+
offset += bytes.length
|
|
225
|
+
}
|
|
226
|
+
for (const key of named.optional) {
|
|
227
|
+
const bytes = readOptional(joinPath(generationDir, requireChildName(key, 'TDB2 file')))
|
|
228
|
+
if (bytes === null) continue
|
|
229
|
+
files.push({ key, offset, len: bytes.length })
|
|
230
|
+
chunks.push(bytes)
|
|
231
|
+
offset += bytes.length
|
|
232
|
+
}
|
|
233
|
+
const config = {}
|
|
234
|
+
if (typeof options.queryPath === 'string') config.queryPath = options.queryPath
|
|
235
|
+
if (typeof options.gspPath === 'string') config.gspPath = options.gspPath
|
|
236
|
+
const info = engine.callBlobIO('tdb2ServeOpen',
|
|
237
|
+
[JSON.stringify(files), JSON.stringify(config)], concatenate(chunks)).envelope
|
|
238
|
+
const handle = info.handle
|
|
239
|
+
return {
|
|
240
|
+
engine,
|
|
241
|
+
info,
|
|
242
|
+
root,
|
|
243
|
+
generation: named.generation,
|
|
244
|
+
generationDir,
|
|
245
|
+
request (bytes) {
|
|
246
|
+
const answer = engine.callBlobIO('storeServeRequest', [handle], bytes)
|
|
247
|
+
return { envelope: answer.envelope, bytes: answer.bytes }
|
|
248
|
+
},
|
|
249
|
+
commit (pending, outcome) {
|
|
250
|
+
const answer = engine.callBlobIO('storeServeCommit', [handle, pending, outcome],
|
|
251
|
+
new Uint8Array(0))
|
|
252
|
+
return { envelope: answer.envelope, bytes: answer.bytes }
|
|
253
|
+
},
|
|
254
|
+
append () {
|
|
255
|
+
return 'a TDB2 database is served read only'
|
|
256
|
+
},
|
|
257
|
+
close () {
|
|
258
|
+
try { engine.call('storeServeClose', [handle]) } catch (_error) { /* already gone */ }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The bytes of a 500 response for a failure of this host. */
|
|
264
|
+
function hostFailure (message) {
|
|
265
|
+
const body = new TextEncoder().encode(`factoidal serve: ${message}\n`)
|
|
266
|
+
const head = new TextEncoder().encode(
|
|
267
|
+
'HTTP/1.1 500 Internal Server Error\r\n' +
|
|
268
|
+
'content-type: text/plain; charset=utf-8\r\n' +
|
|
269
|
+
`content-length: ${body.length}\r\n` +
|
|
270
|
+
'connection: close\r\n\r\n')
|
|
271
|
+
return concatenate([head, body])
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Serve one connection: accumulate bytes, and every time the engine can
|
|
276
|
+
* answer, write its answer and drop the bytes it consumed.
|
|
277
|
+
*/
|
|
278
|
+
function serveConnection (served, socket, onError) {
|
|
279
|
+
let buffered = new Uint8Array(0)
|
|
280
|
+
let waitFor = 0
|
|
281
|
+
let finished = false
|
|
282
|
+
|
|
283
|
+
function fail (error) {
|
|
284
|
+
onError(error)
|
|
285
|
+
finished = true
|
|
286
|
+
socket.end(hostFailure(error && error.message ? error.message : String(error)))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function pump () {
|
|
290
|
+
while (!finished && buffered.length > 0 && buffered.length >= waitFor) {
|
|
291
|
+
let answer
|
|
292
|
+
try {
|
|
293
|
+
answer = served.request(buffered)
|
|
294
|
+
} catch (error) {
|
|
295
|
+
fail(error)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
const envelope = answer.envelope
|
|
299
|
+
if (envelope.state === 'incomplete') {
|
|
300
|
+
waitFor = typeof envelope.need === 'number' ? envelope.need : buffered.length + 1
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
waitFor = 0
|
|
304
|
+
let out = answer.bytes
|
|
305
|
+
if (envelope.state === 'append') {
|
|
306
|
+
const outcome = served.append(envelope, answer.bytes)
|
|
307
|
+
try {
|
|
308
|
+
out = served.commit(envelope.pending, outcome).bytes
|
|
309
|
+
} catch (error) {
|
|
310
|
+
fail(error)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
socket.write(out)
|
|
315
|
+
buffered = buffered.subarray(envelope.consumed)
|
|
316
|
+
if (envelope.close === true) {
|
|
317
|
+
finished = true
|
|
318
|
+
socket.end()
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
socket.on('data', (chunk) => {
|
|
325
|
+
if (finished) return
|
|
326
|
+
buffered = buffered.length === 0 ? new Uint8Array(chunk) : concatenate([buffered, chunk])
|
|
327
|
+
pump()
|
|
328
|
+
})
|
|
329
|
+
socket.on('error', (error) => { finished = true; onError(error) })
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Start serving a collection. Resolves once the socket is bound and the
|
|
334
|
+
* store is open, so a module without the ops, or a collection the engine
|
|
335
|
+
* refuses, is reported by the call that started the server.
|
|
336
|
+
*
|
|
337
|
+
* @param {string} root the collection root
|
|
338
|
+
* @param {object} [options] as `openServedStore`, plus `port`, `host`,
|
|
339
|
+
* `onError`, and `tdb2: true` when `root` is a Jena TDB2 database directory
|
|
340
|
+
* (served read only: https://github.com/danbri/factoidal/issues/834)
|
|
341
|
+
* @returns {Promise<{server: object, served: object, port: number,
|
|
342
|
+
* origin: string, close: Function}>}
|
|
343
|
+
*/
|
|
344
|
+
export async function listen (root, options = {}) {
|
|
345
|
+
const net = await import('node:net')
|
|
346
|
+
const served = options.tdb2 === true
|
|
347
|
+
? await openServedTdb2(root, options)
|
|
348
|
+
: await openServedStore(root, options)
|
|
349
|
+
const onError = typeof options.onError === 'function'
|
|
350
|
+
? options.onError
|
|
351
|
+
: (error) => { console.error(`factoidal serve: ${error && error.message ? error.message : error}`) }
|
|
352
|
+
const sockets = new Set()
|
|
353
|
+
const server = net.createServer((socket) => {
|
|
354
|
+
sockets.add(socket)
|
|
355
|
+
socket.on('close', () => sockets.delete(socket))
|
|
356
|
+
serveConnection(served, socket, onError)
|
|
357
|
+
})
|
|
358
|
+
const port = typeof options.port === 'number' ? options.port : 0
|
|
359
|
+
const host = typeof options.host === 'string' ? options.host : '127.0.0.1'
|
|
360
|
+
await new Promise((resolve, reject) => {
|
|
361
|
+
server.once('error', reject)
|
|
362
|
+
server.listen(port, host, resolve)
|
|
363
|
+
})
|
|
364
|
+
const bound = server.address()
|
|
365
|
+
const origin = `http://${host.indexOf(':') >= 0 ? `[${host}]` : host}:${bound.port}`
|
|
366
|
+
return {
|
|
367
|
+
server,
|
|
368
|
+
served,
|
|
369
|
+
port: bound.port,
|
|
370
|
+
origin,
|
|
371
|
+
close () {
|
|
372
|
+
return new Promise((resolve) => {
|
|
373
|
+
server.close(() => {
|
|
374
|
+
served.close()
|
|
375
|
+
resolve()
|
|
376
|
+
})
|
|
377
|
+
// A persistent connection would hold the server open; end them.
|
|
378
|
+
for (const socket of sockets) socket.destroy()
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* `listen` on a worker thread with a raised call stack, which a large
|
|
386
|
+
* collection needs (`bin/store-worker-host.mjs` measured `storeQueryPlan`
|
|
387
|
+
* alone overflowing Node's default stack on 3,286 blocks). The engine, the
|
|
388
|
+
* served store and the sockets all live on the worker; this thread only
|
|
389
|
+
* starts and stops it.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} root
|
|
392
|
+
* @param {object} options as `listen`, without `engine` and `onError`
|
|
393
|
+
* @returns {Promise<{origin: string, port: number, info: object,
|
|
394
|
+
* close: Function}>}
|
|
395
|
+
*/
|
|
396
|
+
export async function listenOnWorker (root, options = {}) {
|
|
397
|
+
const { Worker } = await import('node:worker_threads')
|
|
398
|
+
const { workerStackMb } = await import('./pack-host.mjs')
|
|
399
|
+
const worker = new Worker(new URL('./serve-worker.mjs', import.meta.url), {
|
|
400
|
+
workerData: { root, options },
|
|
401
|
+
resourceLimits: { stackSizeMb: workerStackMb() }
|
|
402
|
+
})
|
|
403
|
+
const started = await new Promise((resolve, reject) => {
|
|
404
|
+
worker.once('message', resolve)
|
|
405
|
+
worker.once('error', reject)
|
|
406
|
+
})
|
|
407
|
+
if (started.kind !== 'listening') {
|
|
408
|
+
await worker.terminate()
|
|
409
|
+
throw new ServeHostError(started.message, { unknownOp: started.unknownOp === true })
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
origin: started.origin,
|
|
413
|
+
port: started.port,
|
|
414
|
+
info: started.info,
|
|
415
|
+
worker,
|
|
416
|
+
close () {
|
|
417
|
+
return new Promise((resolve) => {
|
|
418
|
+
worker.once('message', () => { worker.terminate().then(() => resolve()) })
|
|
419
|
+
worker.postMessage({ kind: 'close' })
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// of Deno, and the failure is `Maximum call stack size exceeded` rather
|
|
9
9
|
// than an engine refusal. MEASURED 2026-09-05 against the committed
|
|
10
10
|
// module, on a 7,315,251-quad collection of 3,286 blocks in 204 graphs
|
|
11
|
-
// (
|
|
11
|
+
// (a local SKOS corpus): `storeQueryPlan` alone
|
|
12
12
|
// overflows on plain `node`, before a single artifact byte is read, and
|
|
13
13
|
// `node --stack-size=60000` clears the plan, the open and every query.
|
|
14
14
|
// Earlier the same limit was narrowed to any query that materialises
|
package/bin/store.mjs
CHANGED
|
@@ -282,7 +282,7 @@ export function turtleOfNQuads (engine, nquads) {
|
|
|
282
282
|
* The artifact keys a manifest declares, in manifest order: every block, and
|
|
283
283
|
* every index sidecar the entry names. The KEYS come from the engine; this
|
|
284
284
|
* function chooses none of them. A sidecar is what lets a literal search skip
|
|
285
|
-
* the scan (SBM8's
|
|
285
|
+
* the scan (SBM8's LGI3) or a geometry FILTER skip the WKT parse (SBM9's
|
|
286
286
|
* GBI1); a generation that declares none is unaffected. New sidecar roles
|
|
287
287
|
* need no change here: the engine names them under `entry.sidecars`.
|
|
288
288
|
*/
|