@factoidal/core 0.7.1 → 0.11.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.
Files changed (195) hide show
  1. package/CHANGELOG.md +616 -1
  2. package/NOTICE +2 -21
  3. package/README.md +123 -993
  4. package/api.d.ts +53 -0
  5. package/api.js +5 -0
  6. package/api.mjs +56 -0
  7. package/bin/engine.mjs +38 -19
  8. package/bin/ext.mjs +16 -20
  9. package/bin/factoidal.mjs +667 -118
  10. package/bin/pack.mjs +37 -0
  11. package/bin/serve-worker.mjs +32 -0
  12. package/bin/serve.mjs +423 -0
  13. package/bin/store-worker-host.mjs +1 -1
  14. package/bin/store.mjs +1 -1
  15. package/browser.d.ts +114 -507
  16. package/browser.js +155 -2257
  17. package/docs/fuseki-replacement.md +108 -0
  18. package/fn.d.ts +25 -96
  19. package/fn.js +131 -240
  20. package/index.d.ts +620 -260
  21. package/index.js +4 -234
  22. package/index.mjs +180 -84
  23. package/l4-assets/l4factoidal.js +47 -7
  24. package/l4-assets/l4factoidal.mjs +1 -1
  25. package/l4-assets/l4factoidal.wasm +0 -0
  26. package/l4-assets/version.json +11 -7
  27. package/lib/api.mjs +2337 -0
  28. package/lib/browser-engine.mjs +69 -0
  29. package/lib/dataset.mjs +471 -0
  30. package/lib/delta-log-hosts.mjs +191 -0
  31. package/lib/lean-entry.mjs +213 -0
  32. package/lib/pure.mjs +98 -0
  33. package/lib/store.mjs +115 -0
  34. package/lib/version.mjs +5 -0
  35. package/lws/README.md +137 -0
  36. package/lws/persist.mjs +78 -0
  37. package/lws/server.mjs +416 -0
  38. package/package.json +59 -61
  39. package/rdfjs.js +5 -536
  40. package/rdfjs.mjs +517 -0
  41. package/sample-store.d.ts +8 -16
  42. package/sample-store.mjs +39 -29
  43. package/solid/client/README.md +100 -0
  44. package/solid/client/index.mjs +293 -0
  45. package/solid/server/README.md +96 -0
  46. package/solid/server/index.mjs +395 -0
  47. package/store-host/deno.mjs +8 -0
  48. package/store-host/index.mjs +12 -0
  49. package/store-host/node.mjs +8 -0
  50. package/xmpp/README.md +107 -0
  51. package/xmpp/index.mjs +229 -0
  52. package/browser-wasm.js +0 -872
  53. package/factoidal-npm-entry.js +0 -15477
  54. package/factoidal-npm-entry.wasm.assets/code-5a7fc68f2ab1323718b8.wasm +0 -0
  55. package/factoidal-npm-entry.wasm.js +0 -455
  56. package/factoidal.js +0 -13118
  57. package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
  58. package/factoidal.wasm.js +0 -457
  59. package/hacl-init.js +0 -92
  60. package/hacl-wasm/FStar.wasm +0 -0
  61. package/hacl-wasm/Hacl_Bignum.wasm +0 -0
  62. package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
  63. package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
  64. package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
  65. package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
  66. package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
  67. package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
  68. package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
  69. package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
  70. package/hacl-wasm/LowStar_Endianness.wasm +0 -0
  71. package/hacl-wasm/WasmSupport.wasm +0 -0
  72. package/hacl-wasm/api.js +0 -775
  73. package/hacl-wasm/api.json +0 -3787
  74. package/hacl-wasm/layouts.json +0 -1
  75. package/hacl-wasm/loader.js +0 -568
  76. package/hacl-wasm/shell.js +0 -12
  77. package/l4-core.d.ts +0 -159
  78. package/l4-core.js +0 -188
  79. package/l4.d.ts +0 -50
  80. package/l4.js +0 -104
  81. package/lib/api.js +0 -2300
  82. package/lib/engine-js.js +0 -165
  83. package/lib/engine-wasm.js +0 -300
  84. package/sample-store/CURRENT +0 -1
  85. package/sample-store/gen-1/manifest.sbm2 +0 -0
  86. package/sample-store/gen-1/manifest.tsv +0 -14
  87. package/sample-store/gen-1/predicate-0.ibk3 +0 -0
  88. package/sample-store/gen-1/predicate-0.ibk3.merkle +0 -1
  89. package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
  90. package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +0 -1
  91. package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
  92. package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +0 -1
  93. package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
  94. package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
  95. package/sample-store/gen-1/predicate-1.ibk3 +0 -0
  96. package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
  97. package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
  98. package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
  99. package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
  100. package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +0 -1
  101. package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
  102. package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +0 -1
  103. package/sample-store/gen-1/predicate-10.ibk3 +0 -0
  104. package/sample-store/gen-1/predicate-10.ibk3.merkle +0 -1
  105. package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
  106. package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
  107. package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
  108. package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +0 -1
  109. package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
  110. package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +0 -1
  111. package/sample-store/gen-1/predicate-11.ibk3 +0 -0
  112. package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
  113. package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
  114. package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +0 -1
  115. package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
  116. package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +0 -1
  117. package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
  118. package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +0 -1
  119. package/sample-store/gen-1/predicate-12.ibk3 +0 -0
  120. package/sample-store/gen-1/predicate-12.ibk3.merkle +0 -1
  121. package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
  122. package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +0 -1
  123. package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
  124. package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +0 -1
  125. package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
  126. package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +0 -1
  127. package/sample-store/gen-1/predicate-2.ibk3 +0 -0
  128. package/sample-store/gen-1/predicate-2.ibk3.merkle +0 -1
  129. package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
  130. package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +0 -1
  131. package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
  132. package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
  133. package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
  134. package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
  135. package/sample-store/gen-1/predicate-3.ibk3 +0 -0
  136. package/sample-store/gen-1/predicate-3.ibk3.merkle +0 -1
  137. package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
  138. package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +0 -1
  139. package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
  140. package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +0 -1
  141. package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
  142. package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +0 -1
  143. package/sample-store/gen-1/predicate-4.ibk3 +0 -0
  144. package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
  145. package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
  146. package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +0 -1
  147. package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
  148. package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +0 -1
  149. package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
  150. package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +0 -2
  151. package/sample-store/gen-1/predicate-5.ibk3 +0 -0
  152. package/sample-store/gen-1/predicate-5.ibk3.merkle +0 -1
  153. package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
  154. package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +0 -1
  155. package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
  156. package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +0 -1
  157. package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
  158. package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +0 -1
  159. package/sample-store/gen-1/predicate-6.ibk3 +0 -0
  160. package/sample-store/gen-1/predicate-6.ibk3.merkle +0 -1
  161. package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
  162. package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +0 -1
  163. package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
  164. package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +0 -1
  165. package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
  166. package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
  167. package/sample-store/gen-1/predicate-7.ibk3 +0 -0
  168. package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
  169. package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
  170. package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +0 -2
  171. package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
  172. package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +0 -1
  173. package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
  174. package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +0 -1
  175. package/sample-store/gen-1/predicate-8.ibk3 +0 -0
  176. package/sample-store/gen-1/predicate-8.ibk3.merkle +0 -1
  177. package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
  178. package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +0 -1
  179. package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
  180. package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +0 -1
  181. package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
  182. package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +0 -1
  183. package/sample-store/gen-1/predicate-9.ibk3 +0 -0
  184. package/sample-store/gen-1/predicate-9.ibk3.merkle +0 -1
  185. package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
  186. package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +0 -1
  187. package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
  188. package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +0 -3
  189. package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
  190. package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +0 -2
  191. package/select.d.ts +0 -116
  192. package/select.js +0 -492
  193. package/version.json +0 -102
  194. package/wasm.d.ts +0 -130
  195. 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
- // (`/Users/danbri/working/factoidal-skosfull`): `storeQueryPlan` alone
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 LGI1) or a geometry FILTER skip the WKT parse (SBM9'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
  */