@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/factoidal.mjs CHANGED
@@ -20,14 +20,14 @@
20
20
  // `--format turtle` all print documents the engine produced.
21
21
 
22
22
  import {
23
- StoreHostError, atomicReplace, listGeneration, makeDirectory, readWhole,
23
+ StoreHostError, atomicReplace, listGeneration, makeDirectory, readChunk, readWhole,
24
24
  runtime
25
25
  } from '../store-host/index.mjs'
26
26
  import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
27
- import { loadEngine } from './engine.mjs'
28
- import { sampleStoreFacts, sampleStorePath } from '../sample-store.mjs'
27
+ import { hexOfBytes, loadEngine } from './engine.mjs'
29
28
  import { PackError, packSupported, verifyGeneration } from './pack.mjs'
30
29
  import { denoReexec, isStackOverflow, runPack } from './pack-host.mjs'
30
+ import { SAMPLE_TURTLE, sampleStoreFacts } from '../sample-store.mjs'
31
31
  import {
32
32
  planQueryOnWorker, queryStoreOnWorker, workerRouteAvailable
33
33
  } from './store-worker-host.mjs'
@@ -46,23 +46,9 @@ const EXIT_NOT_WIRED = 3
46
46
  const FEED_PROGRESS = 65536
47
47
 
48
48
  const PACK_LAYOUTS = ['ibk3', 'ibk4', 'ibk5']
49
- const PACK_SYNTAXES = ['turtle', 'trig', 'nquads', 'ntriples']
50
- const PACK_SUFFIXES = [
51
- ['.ttl', 'turtle'], ['.turtle', 'turtle'],
52
- ['.trig', 'trig'],
53
- ['.nq', 'nquads'], ['.nquads', 'nquads'],
54
- ['.nt', 'ntriples'], ['.ntriples', 'ntriples']
55
- ]
56
-
57
- // Suffixes the engine parses elsewhere but the packer's streaming fold does
58
- // not read. Naming them is better than "cannot tell the syntax from its
59
- // name", which sends the reader looking for a --syntax value that does not
60
- // exist.
61
- const PACK_UNSUPPORTED_SUFFIXES = [
62
- ['.rdf', 'RDF/XML'], ['.owl', 'RDF/XML'], ['.xml', 'RDF/XML'],
63
- ['.jsonld', 'JSON-LD'], ['.json', 'JSON-LD'], ['.n3', 'Notation3'],
64
- ['.csv', 'CSV'], ['.tsv', 'TSV'], ['.hdt', 'HDT']
65
- ]
49
+ // The syntaxes the packer reads and the file-name suffixes of each are the
50
+ // engine's tables (op engineTables: packSyntaxes, packSuffixes,
51
+ // packUnsupportedSuffixes), https://github.com/danbri/factoidal/issues/894.
66
52
 
67
53
  /** The parent of a path, and its last component. The `activate` hint
68
54
  * printed after a pack needs both; neither is a format decision. */
@@ -91,7 +77,8 @@ function exit (code) {
91
77
  else process.exitCode = code
92
78
  }
93
79
 
94
- function out (line) { console.log(line) }
80
+ let outSilenced = false
81
+ function out (line) { if (!outSilenced) console.log(line) }
95
82
  function err (line) { console.error(line) }
96
83
 
97
84
  // ---------------------------------------------------------------- usage
@@ -102,13 +89,18 @@ usage: factoidal <command> [options]
102
89
 
103
90
  commands:
104
91
  version print the package and engine versions
105
- sample-store print the path of the bundled sample store
92
+ sample-store [DIR] pack a small built-in store and print its path
106
93
  inspect STORE report what the activated manifest commits
107
94
  query STORE [QUERY] evaluate a SPARQL query against a store
108
95
  pack INPUT OUTPUT build one immutable generation from an RDF file
109
96
  activate STORE GENERATION make one generation the activated generation
110
97
  update STORE [UPDATE] apply a SPARQL Update through the delta log
111
98
  compact STORE GENERATION fold the delta log into a new generation
99
+ serve STORE serve SPARQL 1.1 Protocol, Graph Store and
100
+ Service Description over HTTP
101
+ lws-serve DIR serve Linked Web Storage 1.0 over HTTP
102
+ solid-serve DIR serve the Solid Protocol over HTTP
103
+ solid-client OP URL run one Solid client operation
112
104
 
113
105
  global options:
114
106
  -h, --help print this text, or a command's own help
@@ -118,33 +110,30 @@ global options:
118
110
  exit codes:
119
111
  0 success 1 failure 2 usage error 3 not yet wired (${ISSUE})
120
112
 
121
- STORE is a collection root: the directory that holds CURRENT. This package
122
- carries one, so the first query needs no other download:
113
+ STORE is a collection root: the directory that holds CURRENT. Make one from
114
+ an RDF file with pack and activate, or try a small built-in one:
123
115
 
124
116
  factoidal query "$(factoidal sample-store)" 'SELECT * WHERE { ?s ?p ?o } LIMIT 5'`
125
117
 
126
118
  const COMMAND_USAGE = {
127
- version: `factoidal version - print the package and engine versions
119
+ 'sample-store': `factoidal sample-store - pack a small built-in store, print its path
128
120
 
129
- usage: factoidal version [--json]
121
+ usage: factoidal sample-store [DIR] [--json]
130
122
 
131
- Prints the npm package version, the Lean engine's WebAssembly digest as
132
- recorded by its build, and which host-I/O implementation is loaded.`,
123
+ Packs the few triples held in sample-store.mjs (no data file ships) into a
124
+ store under DIR, or under the OS temporary directory, activates it, and
125
+ prints the collection root. A DIR that already holds CURRENT is reused.
133
126
 
134
- 'sample-store': `factoidal sample-store - print the bundled store's path
127
+ factoidal query "$(factoidal sample-store)" \\
128
+ 'SELECT ?c ?l WHERE { ?c <http://www.w3.org/2004/02/skos/core#prefLabel> ?l }'`,
135
129
 
136
- usage: factoidal sample-store [--json]
130
+ version: `factoidal version - print the package and engine versions
137
131
 
138
- Prints the collection root of the Shardborough store this package
139
- carries, so a fresh install can query something at once:
132
+ usage: factoidal version [--json]
140
133
 
141
- factoidal inspect "$(factoidal sample-store)"
142
- factoidal query "$(factoidal sample-store)" \\
143
- 'SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
134
+ Prints the npm package version, the Lean engine's WebAssembly digest as
135
+ recorded by its build, and which host-I/O implementation is loaded.`,
144
136
 
145
- The store holds five IPTC NewsCodes vocabularies (CC BY 4.0; see NOTICE)
146
- packed into IBK3 predicate blocks. --json adds what was recorded when it
147
- was packed.`,
148
137
 
149
138
  inspect: `factoidal inspect - report what a store's manifest commits
150
139
 
@@ -200,14 +189,16 @@ every later query from what it retained.
200
189
 
201
190
  formats:
202
191
  table a human display of the engine's SPARQL Query Results JSON;
203
- ASK prints true or false, CONSTRUCT prints its N-Triples
192
+ ASK prints true or false, CONSTRUCT and DESCRIBE print their
193
+ N-Triples
204
194
  json SELECT prints the engine's SPARQL 1.1 Query Results JSON
205
- document; ASK and CONSTRUCT print the operation's envelope,
206
- because the operation answers those two with a boolean and a
207
- serialized graph rather than with a results document
208
- nquads CONSTRUCT only: the graph the engine serialized
209
- turtle CONSTRUCT only: that graph through the engine's own Turtle
210
- writer, which flattens named graphs into the default graph
195
+ document; ASK, CONSTRUCT and DESCRIBE print the operation's
196
+ envelope, because the operation answers those with a boolean
197
+ or a serialized graph rather than with a results document
198
+ nquads CONSTRUCT and DESCRIBE only: the graph the engine serialized
199
+ turtle CONSTRUCT and DESCRIBE only: that graph through the engine's
200
+ own Turtle writer, which flattens named graphs into the default
201
+ graph
211
202
 
212
203
  not available, and why:
213
204
  --base IRI the store query operation takes no base argument;
@@ -215,8 +206,6 @@ not available, and why:
215
206
  xml, csv, tsv the engine has no operation that writes the SPARQL
216
207
  Results XML, CSV or TSV documents, and writing one
217
208
  here would be a second serializer (iron rule 7)
218
- DESCRIBE the engine answers "DESCRIBE is not supported by the
219
- npm entry yet"
220
209
 
221
210
  A store carrying a non-empty delta log is not served by this path: the
222
211
  operation reads the manifest's committed artifacts only. Use the native
@@ -279,7 +268,186 @@ compacted epoch. It does not activate unless --activate is given.
279
268
 
280
269
  options:
281
270
  --activate activate the new generation when it verifies
282
- --json emit one JSON object`
271
+ --json emit one JSON object`,
272
+
273
+ serve: `factoidal serve - a SPARQL 1.1 endpoint over an activated store
274
+
275
+ usage: factoidal serve STORE [--port N] [--host ADDRESS] [options]
276
+ factoidal serve --file DATA /DATASET [--port N] [options]
277
+ factoidal serve --tdb2 DIR /DATASET [--port N] [options]
278
+
279
+ The second form matches fuseki-server --file DATA /DATASET: DATA is packed
280
+ into a temporary collection (layout ibk5 unless --layout is given), served
281
+ at /DATASET/sparql (also /DATASET/query and /DATASET/update) and
282
+ /DATASET/data (also /DATASET/get, read only). Updates are lost on exit.
283
+
284
+ The third form matches fuseki-server --loc DIR /DATASET for an Apache Jena
285
+ TDB2 database: DIR is read in place, read only. SPARQL query and Graph Store
286
+ GET work; an update or a Graph Store write answers 501 with a message. The
287
+ current Data-NNNN generation is served; a database whose journal.jrnl is not
288
+ empty (Jena has not replayed it) is refused. The Lean decoder reads the
289
+ indexes and node table (formal/lean4/L4Factoidal/Tdb2/Read.lean); every
290
+ quad is held in memory while the server runs.
291
+
292
+ Serves the activated generation of STORE, with its delta log, over HTTP:
293
+
294
+ GET/POST /sparql SPARQL 1.1 Protocol query and update
295
+ (GET /sparql with no query string: the SPARQL 1.1
296
+ Service Description of the endpoint)
297
+ /data, /data/... SPARQL 1.1 Graph Store HTTP Protocol (?default,
298
+ ?graph=IRI, or a path under /data naming the graph)
299
+
300
+ options:
301
+ --port N the port to bind; 0 takes an ephemeral one (default 3030)
302
+ --host ADDRESS the address to bind (default 127.0.0.1)
303
+ --query-path PATH the query and update endpoint (default /sparql)
304
+ --gsp-path PATH the Graph Store endpoint (default /data)
305
+ --file DATA pack and serve an RDF file (see the second form)
306
+ --tdb2 DIR serve a Jena TDB2 database read only (third form)
307
+ --layout LAYOUT with --file: ibk3, ibk4 or ibk5 (default ibk5)
308
+ --no-worker serve on this thread instead of a worker thread with a
309
+ raised call stack (large collections need the worker)
310
+ --quiet print the origin only
311
+
312
+ An update, or a Graph Store PUT, POST or DELETE, is appended to the
313
+ activated generation's delta log as one batch before the response is sent,
314
+ and is visible to the next request. An IBK3 (triples) generation accepts
315
+ changes to the default graph only, because its delta log is replayed and
316
+ compacted for that graph only; a change to a named graph answers 501. A
317
+ quad generation (ibk4, ibk5) accepts every graph. Fold the log into a new
318
+ generation with l4block-shard-compact, activate it, and restart the server.
319
+
320
+ The server makes no protocol decision: the request framing, routing,
321
+ status codes, content negotiation, evaluation and the delta-log batch are
322
+ decided by formal/lean4/L4Factoidal/HTTP/Wire.lean and
323
+ formal/lean4/L4Factoidal/SPARQL/StoreEndpoint.lean through the
324
+ storeServe operations. A WebAssembly module built before those operations
325
+ landed makes this command exit 3 and say so.
326
+
327
+ Specifications: https://www.w3.org/TR/sparql11-protocol/
328
+ https://www.w3.org/TR/sparql11-http-rdf-update/
329
+ https://www.w3.org/TR/sparql11-service-description/`,
330
+
331
+ 'lws-serve': `factoidal lws-serve - serve Linked Web Storage 1.0 over HTTP
332
+
333
+ usage: factoidal lws-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
334
+ [--owner WEBID] [--root-acl FILE] [--audience IRI]
335
+ [--port N] [--host ADDRESS] [--base IRI]
336
+
337
+ Starts a Node HTTP server that answers every request through the engine's
338
+ lwsStep operation.
339
+
340
+ DIR names the storage directory. The storage is kept in the engine
341
+ handle and written to DIR/lws-state.json after every request that
342
+ changed it (temporary file, fsync, rename), and read back on the next
343
+ start. The engine decides the snapshot format and refuses a snapshot
344
+ path with a dot segment or other traversal form
345
+ (formal/lean4/L4Factoidal/LWS/Snapshot.lean).
346
+
347
+ --port N the port to bind; 0 takes an ephemeral one (default 3000)
348
+ --host ADDRESS the address to bind (default 127.0.0.1)
349
+ --base IRI the absolute IRI the storage root maps to
350
+ (default: the bound origin)
351
+
352
+ Authorization (LWS editor's draft, section Authorization; one of the three
353
+ is required):
354
+ --issuer IRI trust this authorization server; several may be
355
+ given separated by commas. Its metadata is read from
356
+ IRI/.well-known/lws-configuration and its keys from
357
+ the jwks_uri there, when a token first needs them.
358
+ --auth-config FILE the engine's "auth" configuration as JSON:
359
+ {"issuers":[{"issuer":IRI,"jwks":{...}?}],
360
+ "audience":IRI?, "leeway":SECONDS?,
361
+ "refreshInterval":SECONDS?}
362
+ --insecure-no-auth development only: no authorization at all; every
363
+ request is served. Never the default.
364
+ --owner WEBID the storage owner: a new storage gets a root ACL
365
+ that gives this agent Read, Write and Control
366
+ --root-acl FILE a Turtle root ACL for a new storage instead
367
+ --audience IRI the aud value tokens must carry (default: the
368
+ storage root IRI)
369
+
370
+ Requests carry an access token as "Authorization: Bearer TOKEN" (RFC 6750).
371
+ The engine verifies its JWS signature (ES256, RS256 or EdDSA, with the
372
+ vendored HACL* code), its iss, aud, exp, nbf and iat, and then decides
373
+ access with Web Access Control over the storage's ACL resources. A
374
+ request without a valid token answers 401 with
375
+ WWW-Authenticate: Bearer resource_metadata="..." (RFC 9728), and the
376
+ metadata document is served at /.well-known/oauth-protected-resource.
377
+
378
+ The server makes no protocol decision: status codes, Link relations,
379
+ Last-Modified, the PATCH blank-node refusal and every authorization
380
+ decision are answered by formal/lean4/L4Factoidal/LWS/. A WebAssembly module built before those
381
+ operations landed makes this command exit 3 and say so.
382
+
383
+ Specification: https://w3c.github.io/lws-protocol/lws10-core/`,
384
+
385
+ 'solid-serve': `factoidal solid-serve - serve the Solid Protocol over HTTP
386
+
387
+ usage: factoidal solid-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
388
+ [--owner WEBID] [--port N] [--host ADDRESS] [--base IRI]
389
+
390
+ Starts a Node HTTP server that answers every request through the engine's
391
+ solidStep operation.
392
+
393
+ DIR names the storage directory. The storage is kept in the engine
394
+ handle and written to DIR/lws-state.json after every request that
395
+ changed it (temporary file, fsync, rename), and read back on the next
396
+ start. The engine decides the snapshot format and refuses a snapshot
397
+ path with a dot segment or other traversal form
398
+ (formal/lean4/L4Factoidal/LWS/Snapshot.lean).
399
+
400
+ --port N the port to bind; 0 takes an ephemeral one (default 3000)
401
+ --host ADDRESS the address to bind (default 127.0.0.1)
402
+ --base IRI the absolute IRI the storage root maps to
403
+ (default: the bound origin)
404
+ --owner WEBID the WebID the storage advertises as its owner; under
405
+ authentication it gets acl:Read, acl:Write and
406
+ acl:Control on the root (Web Access Control §3.2)
407
+ --issuer IRI trust this Solid-OIDC issuer (comma separated for more).
408
+ The engine verifies each DPoP-bound access token
409
+ (Solid-OIDC §8, §9.3; RFC 9449): signature under the
410
+ issuer's keys found by OpenID Connect Discovery, aud
411
+ "solid", webid, exp, cnf.jkt bound to the DPoP key,
412
+ htm, htu, iat and a jti replay cache, and that the
413
+ WebID profile lists the issuer (solid:oidcIssuer).
414
+ --auth-config FILE the same as JSON: {"issuers":[{"issuer":IRI,
415
+ "jwks":{...}?}], "leeway"?, "iatWindow"?,
416
+ "refreshInterval"?, "checkProfileIssuer"?}
417
+ --insecure-no-auth development only: no authentication; Web Access
418
+ Control is not enforced
419
+
420
+ The server makes no protocol decision. Storage discovery, containment
421
+ triples, slash semantics, the Allow and Accept-* headers, the PUT/POST/
422
+ PATCH/DELETE rules, the acl and describedby lifecycle, N3 Patch, Web
423
+ Access Control, CORS and the LDN inbox are answered by
424
+ formal/lean4/L4Factoidal/Solid/Server/. A WebAssembly module built before
425
+ those operations landed makes this command exit 3 and say so.
426
+
427
+ Specification: https://solidproject.org/TR/protocol`,
428
+
429
+ 'solid-client': `factoidal solid-client - run one Solid client operation
430
+
431
+ usage: factoidal solid-client <get|put|post|delete|discover> URL [options]
432
+
433
+ Every request is built by the engine's solidClientRequest operation and
434
+ every response is read by solidClientResponse. This command supplies a
435
+ socket and prints what the engine answered.
436
+
437
+ The five verbs are aliases for the operation kinds of the wasm ABI; the
438
+ kind names are accepted directly as well.
439
+
440
+ get read put replace post create
441
+ delete delete discover discoverStorage
442
+ (also: patch, readProfile)
443
+
444
+ --body TEXT the request body for put, post and patch
445
+ --file PATH read the request body from a file
446
+ --content-type M the body's media type
447
+ --interpret KIND one of storage, containment, auxiliaries, profile,
448
+ wacAllow, used when the engine names none
449
+
450
+ Specification: https://solidproject.org/TR/protocol`
283
451
  }
284
452
 
285
453
  // ------------------------------------------------------------- arguments
@@ -334,7 +502,12 @@ const VALUE_OPTIONS = {
334
502
  pack: new Set(['layout', 'syntax', 'chunk-bytes', 'base', 'batch-bytes']),
335
503
  activate: new Set([]),
336
504
  update: new Set(['update', 'file']),
337
- compact: new Set([])
505
+ compact: new Set([]),
506
+ serve: new Set(['port', 'host', 'query-path', 'gsp-path', 'file', 'layout', 'tdb2']),
507
+ 'lws-serve': new Set(['port', 'host', 'base', 'issuer', 'auth-config', 'owner', 'root-acl',
508
+ 'audience']),
509
+ 'solid-serve': new Set(['port', 'host', 'base', 'owner', 'issuer', 'auth-config']),
510
+ 'solid-client': new Set(['body', 'file', 'content-type', 'interpret'])
338
511
  }
339
512
 
340
513
  // ------------------------------------------------------------- commands
@@ -348,85 +521,142 @@ function readJsonFile (path) {
348
521
  return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
349
522
  }
350
523
 
351
- function commandVersion (options) {
524
+ async function sha256Hex (bytes) {
525
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)
526
+ return hexOfBytes(new Uint8Array(digest))
527
+ }
528
+
529
+ // `factoidal version`: what l4-assets/version.json says (the release stamp,
530
+ // written by tools/fetch-builds.sh --set web from the factoidal-builds
531
+ // release that built the wasm), what the installed wasm files hash to, and
532
+ // the loaded engine's own version(). A stamp whose sha256 differs from the
533
+ // installed wasm is an error (exit 1): the stamp then describes another
534
+ // build. A source checkout carries the unstamped placeholder.
535
+ async function commandVersion (options) {
352
536
  const root = packageDirectory()
353
537
  const own = readJsonFile(joinPath(root, 'package.json'))
354
- let engine = null
538
+ let stamp = null
355
539
  try {
356
- engine = readJsonFile(joinPath(root, 'l4-assets/version.json'))
540
+ stamp = readJsonFile(joinPath(root, 'l4-assets/version.json'))
357
541
  } catch (_error) {
358
- engine = null
542
+ stamp = null
543
+ }
544
+ const stamped = stamp !== null && stamp.stamped === true
545
+ const installed = {}
546
+ for (const [key, file] of [['wasm', 'l4factoidal.wasm']]) {
547
+ try {
548
+ const bytes = readWhole(joinPath(root, `l4-assets/${file}`))
549
+ installed[key] = { sha256: await sha256Hex(bytes), bytes: bytes.length }
550
+ } catch (_error) {
551
+ installed[key] = null
552
+ }
553
+ }
554
+ let engineVersion = null
555
+ let engineError = null
556
+ if (installed.wasm !== null) {
557
+ try {
558
+ engineVersion = (await loadEngine()).version()
559
+ } catch (error) {
560
+ engineError = String(error && error.message ? error.message : error)
561
+ }
562
+ }
563
+ const mismatches = []
564
+ if (stamped) {
565
+ for (const [key, shaKey, bytesKey] of [['wasm', 'wasmSha256', 'wasmBytes']]) {
566
+ if (installed[key] === null) continue
567
+ if (stamp[shaKey] !== installed[key].sha256 || stamp[bytesKey] !== installed[key].bytes) {
568
+ mismatches.push(`${key}: version.json says sha256 ${stamp[shaKey]} (${stamp[bytesKey]} bytes), ` +
569
+ `installed file is ${installed[key].sha256} (${installed[key].bytes} bytes)`)
570
+ }
571
+ }
572
+ if (engineVersion !== null && typeof stamp.engineVersion === 'string' &&
573
+ stamp.engineVersion !== engineVersion) {
574
+ mismatches.push(`engine: version.json says ${stamp.engineVersion}, the loaded wasm says ${engineVersion}`)
575
+ }
359
576
  }
360
577
  const report = {
361
578
  package: own.name,
362
579
  version: own.version,
363
580
  host: runtime,
364
- engine: engine === null
581
+ stamp: stamp === null
365
582
  ? null
366
- : {
367
- engine: engine.engine,
368
- version: engine.version,
369
- gitSha: engine.gitSha,
370
- leanToolchain: engine.leanToolchain,
371
- wasmSha256: engine.wasmSha256,
372
- wasmBytes: engine.wasmBytes
373
- }
583
+ : stamped
584
+ ? {
585
+ stamped: true,
586
+ engineVersion: stamp.engineVersion,
587
+ gitSha: stamp.gitSha,
588
+ builtAt: stamp.builtAt,
589
+ release: stamp.release,
590
+ leanToolchain: stamp.leanToolchain,
591
+ wasmSha256: stamp.wasmSha256,
592
+ wasmBytes: stamp.wasmBytes
593
+ }
594
+ : { stamped: false, status: stamp.status || 'stamped at release' },
595
+ installed,
596
+ engineVersion,
597
+ engineError,
598
+ mismatches
374
599
  }
375
600
  if (options.json) {
376
601
  out(JSON.stringify(report, null, 2))
377
- return EXIT_OK
378
- }
379
- out(`${report.package} ${report.version}`)
380
- out(`host-io ${report.host}`)
381
- if (report.engine !== null) {
382
- out(`engine ${report.engine.engine} ${report.engine.version} (${report.engine.leanToolchain})`)
383
- out(`wasm sha256 ${report.engine.wasmSha256} (${report.engine.wasmBytes} bytes)`)
384
602
  } else {
385
- out('engine (no l4-assets/version.json in this install)')
603
+ out(`${report.package} ${report.version}`)
604
+ out(`host-io ${report.host}`)
605
+ if (stamp === null) {
606
+ out('stamp: no l4-assets/version.json in this install')
607
+ } else if (!stamped) {
608
+ out('stamp: not stamped (l4-assets/version.json is the source placeholder; ' +
609
+ 'it is stamped at release by tools/fetch-builds.sh --set web)')
610
+ } else {
611
+ out(`stamp: built from ${stamp.gitSha} at ${stamp.builtAt}` +
612
+ (stamp.release ? ` (${stamp.release})` : ''))
613
+ out(`stamp: engine ${stamp.engineVersion} (${stamp.leanToolchain})`)
614
+ out(`stamp: wasm sha256 ${stamp.wasmSha256} (${stamp.wasmBytes} bytes)`)
615
+ }
616
+ for (const [key, label] of [['wasm', 'wasm']]) {
617
+ out(installed[key] === null
618
+ ? `installed ${label}: absent`
619
+ : `installed ${label} sha256 ${installed[key].sha256} (${installed[key].bytes} bytes)`)
620
+ }
621
+ out(engineVersion !== null
622
+ ? `engine version() ${engineVersion}`
623
+ : `engine version() unavailable${engineError ? ': ' + engineError : ''}`)
386
624
  }
387
- return EXIT_OK
388
- }
389
-
390
- function commandSampleStore (options) {
391
- const path = sampleStorePath()
392
- if (options.json === true) {
393
- out(JSON.stringify({ path, ...sampleStoreFacts }, null, 2))
394
- return EXIT_OK
625
+ if (mismatches.length > 0) {
626
+ for (const m of mismatches) err(`factoidal version: STAMP MISMATCH ${m}`)
627
+ return EXIT_FAILURE
395
628
  }
396
- out(path)
397
629
  return EXIT_OK
398
630
  }
399
631
 
632
+
400
633
  // ------------------------------------------------------------ rendering
401
634
 
402
635
  /**
403
636
  * One graph name as the manifest reports it. `{"kind":"default"}` is the
404
637
  * default graph; the operation marks it that way so a host never has to
405
- * recognise a reserved IRI.
638
+ * recognise a reserved IRI. The engine writes the N-Triples form of a name
639
+ * (op termsToNTriples).
406
640
  */
407
- function graphLabel (graph) {
408
- if (graph.kind === 'default') return 'default'
409
- if (graph.kind === 'iri') return `<${graph.value}>`
410
- if (graph.kind === 'bnode') return `_:${graph.value}`
411
- return JSON.stringify(graph)
641
+ function graphLabels (engine, graphs) {
642
+ const terms = graphs.filter((g) => g.kind !== 'default')
643
+ .map((g) => ({ type: g.kind === 'iri' ? 'uri' : 'bnode', value: g.value }))
644
+ const tokens = terms.length === 0 ? [] : engine.call('termsToNTriples', [JSON.stringify(terms)]).tokens
645
+ let k = 0
646
+ return graphs.map((g) => (g.kind === 'default' ? 'default' : tokens[k++]))
412
647
  }
413
648
 
414
649
  /**
415
- * One SPARQL Query Results JSON term as a table cell. This is a display,
416
- * not a serialization: `--format json` prints the engine's own document
417
- * and `--format nquads` prints the engine's own N-Triples.
650
+ * The SPARQL Query Results JSON terms of a table, as display cells: the
651
+ * engine writes each term's canonical N-Triples form (op termsToNTriples).
652
+ * `--format json` prints the engine's own document.
418
653
  */
419
- function cellOfTerm (term) {
420
- if (term === undefined || term === null) return ''
421
- if (term.type === 'uri') return `<${term.value}>`
422
- if (term.type === 'bnode') return `_:${term.value}`
423
- if (term.type === 'literal') {
424
- const lang = term['xml:lang']
425
- if (typeof lang === 'string' && lang.length > 0) return `"${term.value}"@${lang}`
426
- if (typeof term.datatype === 'string') return `"${term.value}"^^<${term.datatype}>`
427
- return `"${term.value}"`
428
- }
429
- return JSON.stringify(term)
654
+ function cellsOfRows (engine, rows) {
655
+ const terms = []
656
+ for (const row of rows) for (const t of row) if (t !== undefined && t !== null) terms.push(t)
657
+ const tokens = terms.length === 0 ? [] : engine.call('termsToNTriples', [JSON.stringify(terms)]).tokens
658
+ let k = 0
659
+ return rows.map((row) => row.map((t) => (t === undefined || t === null ? '' : tokens[k++])))
430
660
  }
431
661
 
432
662
  function plural (count, noun) {
@@ -502,7 +732,7 @@ async function commandInspect (positional, options) {
502
732
  String(entry.rows),
503
733
  String(entry.bytes),
504
734
  entry.blockKind,
505
- entry.graphs.length === 0 ? '-' : entry.graphs.map(graphLabel).join(' '),
735
+ entry.graphs.length === 0 ? '-' : graphLabels(engine, entry.graphs).join(' '),
506
736
  entry.predicate
507
737
  ].concat(zoned
508
738
  ? [String((entry.blobRefs ?? []).length), zoneCell(entry.subjectZone)]
@@ -674,11 +904,11 @@ function renderQueryResult (engine, result, format, limit, quiet) {
674
904
  return EXIT_OK
675
905
  }
676
906
  if (format !== 'table') {
677
- err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is a SELECT`)
907
+ err(`factoidal query: --format ${format} needs a CONSTRUCT or DESCRIBE query; this one is a SELECT`)
678
908
  return EXIT_FAILURE
679
909
  }
680
910
  const shown = limit === null ? bindings : bindings.slice(0, limit)
681
- printTable(vars, shown.map((row) => vars.map((name) => cellOfTerm(row[name]))))
911
+ printTable(vars, cellsOfRows(engine, shown.map((row) => vars.map((name) => row[name]))))
682
912
  if (shown.length < bindings.length) {
683
913
  err(`showing ${shown.length} of ${bindings.length} rows (--limit ${limit})`)
684
914
  } else if (!quiet) {
@@ -694,13 +924,13 @@ function renderQueryResult (engine, result, format, limit, quiet) {
694
924
  return EXIT_OK
695
925
  }
696
926
  if (format !== 'table') {
697
- err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is an ASK`)
927
+ err(`factoidal query: --format ${format} needs a CONSTRUCT or DESCRIBE query; this one is an ASK`)
698
928
  return EXIT_FAILURE
699
929
  }
700
930
  out(result.boolean ? 'true' : 'false')
701
931
  return EXIT_OK
702
932
  }
703
- if (result.kind === 'construct') {
933
+ if (result.kind === 'construct' || result.kind === 'describe') {
704
934
  if (format === 'json') {
705
935
  out(JSON.stringify(result, null, 2))
706
936
  return EXIT_OK
@@ -717,32 +947,51 @@ function renderQueryResult (engine, result, format, limit, quiet) {
717
947
  return EXIT_FAILURE
718
948
  }
719
949
 
720
- /** The syntax tag for an input, from --syntax or from the file name. The
721
- * engine is what actually decides how to read the bytes; this only picks
722
- * which of its parsers to name. */
723
- function packSyntax (input, options) {
950
+ /** The first `length` bytes of a file, as text, for content detection. */
951
+ function readHead (path, length) {
952
+ return new TextDecoder('utf-8', { fatal: false }).decode(readChunk(path, 0, length))
953
+ }
954
+
955
+ /** The syntax tag for an input: --syntax when given, else the engine's
956
+ * suffix table (op engineTables), else the engine's content detection
957
+ * (op detectFormat) on the first 64 KiB of the file. */
958
+ function packSyntax (engine, input, options) {
959
+ const tables = engine.call('engineTables', []).tables
960
+ const syntaxes = tables.packSyntaxes
724
961
  if (typeof options.syntax === 'string') {
725
962
  const syntax = options.syntax.toLowerCase()
726
- if (PACK_SYNTAXES.indexOf(syntax) >= 0) return syntax
727
- throw new UsageError(`--syntax ${options.syntax} is not one of ${PACK_SYNTAXES.join(', ')}`)
963
+ if (syntaxes.indexOf(syntax) >= 0) return syntax
964
+ throw new UsageError(`--syntax ${options.syntax} is not one of ${syntaxes.join(', ')}`)
728
965
  }
729
966
  const lower = input.toLowerCase()
730
- for (const [suffix, syntax] of PACK_SUFFIXES) {
967
+ for (const [suffix, syntax] of Object.entries(tables.packSuffixes)) {
731
968
  if (lower.endsWith(suffix)) return syntax
732
969
  }
733
- for (const [suffix, name] of PACK_UNSUPPORTED_SUFFIXES) {
970
+ for (const [suffix, name] of Object.entries(tables.packUnsupportedSuffixes)) {
734
971
  if (lower.endsWith(suffix)) {
735
972
  throw new UsageError(
736
973
  `pack does not read ${name}. The packer's streaming fold reads ` +
737
- `${PACK_SYNTAXES.join(', ')} only. Convert the file first, for ` +
974
+ `${syntaxes.join(', ')} only. Convert the file first, for ` +
738
975
  "example with: factoidal parse FILE --out nquads")
739
976
  }
740
977
  }
978
+ let head = ''
979
+ try {
980
+ head = readHead(input, 65536)
981
+ } catch (_error) {
982
+ head = ''
983
+ }
984
+ if (head !== '') {
985
+ const tag = engine.call('detectFormat', [head]).format
986
+ const syntax = tag.replace(/12$/, '')
987
+ if (syntaxes.indexOf(syntax) >= 0) return syntax
988
+ }
741
989
  throw new UsageError(
742
- `cannot tell the syntax of ${input} from its name; give --syntax ` +
743
- `(${PACK_SYNTAXES.join(', ')})`)
990
+ `cannot tell the syntax of ${input} from its name or its content; give --syntax ` +
991
+ `(${syntaxes.join(', ')})`)
744
992
  }
745
993
 
994
+
746
995
  /**
747
996
  * The base IRI relative IRIs in the source resolve against.
748
997
  *
@@ -773,7 +1022,7 @@ function packLayout (options) {
773
1022
  async function commandPack (positional, options) {
774
1023
  if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
775
1024
  const [input, output] = positional
776
- const syntax = packSyntax(input, options)
1025
+ const syntax = packSyntax(await loadEngine(), input, options)
777
1026
  const layout = packLayout(options)
778
1027
  // The pack fold needs a bigger call stack than either runtime gives by
779
1028
  // default (https://github.com/danbri/factoidal/issues/649). Under Node
@@ -835,6 +1084,34 @@ async function commandPack (positional, options) {
835
1084
  return EXIT_OK
836
1085
  }
837
1086
 
1087
+ async function commandSampleStore (positional, options) {
1088
+ if (positional.length > 1) throw new UsageError('sample-store takes at most one DIR')
1089
+ const { mkdtempSync, writeFileSync, existsSync } = await import('node:fs')
1090
+ const { tmpdir } = await import('node:os')
1091
+ const root = positional.length === 1
1092
+ ? positional[0]
1093
+ : mkdtempSync(joinPath(tmpdir(), 'factoidal-sample-store-'))
1094
+ if (!existsSync(joinPath(root, 'CURRENT'))) {
1095
+ makeDirectory(joinPath(root, 'gen-1'))
1096
+ const source = joinPath(root, 'sample.ttl')
1097
+ writeFileSync(source, SAMPLE_TURTLE)
1098
+ outSilenced = true
1099
+ try {
1100
+ let code = await commandPack([source, joinPath(root, 'gen-1')], { quiet: true })
1101
+ if (code === EXIT_OK) code = await commandActivate([root, 'gen-1'], { quiet: true })
1102
+ if (code !== EXIT_OK) return code
1103
+ } finally {
1104
+ outSilenced = false
1105
+ }
1106
+ }
1107
+ if (options.json === true) {
1108
+ out(JSON.stringify({ path: root, ...sampleStoreFacts }, null, 2))
1109
+ } else {
1110
+ out(root)
1111
+ }
1112
+ return EXIT_OK
1113
+ }
1114
+
838
1115
  async function commandActivate (positional, options) {
839
1116
  if (positional.length !== 2) throw new UsageError('activate needs STORE and GENERATION')
840
1117
  const [root, generation] = positional
@@ -906,15 +1183,287 @@ function commandCompact (positional, _options) {
906
1183
  return notWired('compact', 'Compaction is stage 4 of the milestone.')
907
1184
  }
908
1185
 
1186
+ // ------------------------------------------------- the protocol servers
1187
+ // These commands hand a socket to a host module and print where it is
1188
+ // listening. They decide nothing about the protocol they serve; see
1189
+ // npm/factoidal/lws/README.md and npm/factoidal/solid/*/README.md.
1190
+ // https://github.com/danbri/factoidal/issues/659
1191
+
1192
+ function servePort (options, fallback) {
1193
+ if (options.port === undefined) return fallback
1194
+ const port = Number(options.port)
1195
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
1196
+ throw new UsageError(`--port must be an integer from 0 to 65535, not "${options.port}"`)
1197
+ }
1198
+ return port
1199
+ }
1200
+
1201
+ function serveHost (options) {
1202
+ if (options.host === undefined) return '127.0.0.1'
1203
+ if (typeof options.host !== 'string' || options.host.length === 0) {
1204
+ throw new UsageError('--host needs an address')
1205
+ }
1206
+ return options.host
1207
+ }
1208
+
1209
+ /** Keep the process alive until the runtime is interrupted. */
1210
+ function untilInterrupted (running, quiet) {
1211
+ return new Promise((resolve) => {
1212
+ const stop = async () => {
1213
+ if (!quiet) err('')
1214
+ await running.close()
1215
+ resolve(EXIT_OK)
1216
+ }
1217
+ if (isDeno) {
1218
+ globalThis.Deno.addSignalListener('SIGINT', stop)
1219
+ } else {
1220
+ process.on('SIGINT', stop)
1221
+ process.on('SIGTERM', stop)
1222
+ }
1223
+ })
1224
+ }
1225
+
1226
+ async function commandServe (positional, options) {
1227
+ const quiet = options.quiet === true
1228
+ // The Apache Jena Fuseki form: `serve --file DATA /ds`. The file is
1229
+ // packed into a new collection in a temporary directory (ibk5 unless
1230
+ // --layout says otherwise, so every graph accepts writes), activated, and
1231
+ // served at /ds/sparql and /ds/data; /ds/query, /ds/update and /ds/get
1232
+ // are answered too (StoreEndpoint.datasetPrefix). Updates go to that
1233
+ // temporary collection and are lost when the process ends, as with
1234
+ // fuseki-server --file. docs/fuseki-replacement.md.
1235
+ if (typeof options.file === 'string') {
1236
+ if (positional.length !== 1 || !positional[0].startsWith('/')) {
1237
+ throw new UsageError('serve --file DATA needs one dataset name, such as /ds')
1238
+ }
1239
+ const dsName = positional[0].replace(/\/+$/, '')
1240
+ const { mkdtempSync } = await import('node:fs')
1241
+ const { tmpdir } = await import('node:os')
1242
+ const { join } = await import('node:path')
1243
+ const root = mkdtempSync(join(tmpdir(), 'factoidal-serve-'))
1244
+ const packOptions = { quiet: true, layout: typeof options.layout === 'string' ? options.layout : 'ibk5' }
1245
+ let rc = await commandPack([options.file, join(root, 'gen-1')], packOptions)
1246
+ if (rc !== 0) return rc
1247
+ rc = await commandActivate([root, 'gen-1'], { quiet: true })
1248
+ if (rc !== 0) return rc
1249
+ if (!quiet) err(`serve: packed ${options.file} into ${root} (temporary; updates are lost on exit)`)
1250
+ positional = [root]
1251
+ options = { ...options, 'query-path': `${dsName}/sparql`, 'gsp-path': `${dsName}/data` }
1252
+ }
1253
+ // The Apache Jena Fuseki form: `serve --tdb2 DIR /ds`, a TDB2 database
1254
+ // read in place and read only (https://github.com/danbri/factoidal/issues/834).
1255
+ const tdb2 = typeof options.tdb2 === 'string'
1256
+ if (tdb2) {
1257
+ if (positional.length !== 1 || !positional[0].startsWith('/')) {
1258
+ throw new UsageError('serve --tdb2 DIR needs one dataset name, such as /ds')
1259
+ }
1260
+ const dsName = positional[0].replace(/\/+$/, '')
1261
+ positional = [options.tdb2]
1262
+ options = { ...options, 'query-path': `${dsName}/sparql`, 'gsp-path': `${dsName}/data` }
1263
+ }
1264
+ if (positional.length !== 1) throw new UsageError('serve needs one STORE')
1265
+ const { listen, listenOnWorker, ServeHostError } = await import('./serve.mjs')
1266
+ const serveOptions = {
1267
+ port: servePort(options, 3030),
1268
+ host: serveHost(options),
1269
+ ...(tdb2 ? { tdb2: true } : {}),
1270
+ ...(typeof options['query-path'] === 'string' ? { queryPath: options['query-path'] } : {}),
1271
+ ...(typeof options['gsp-path'] === 'string' ? { gspPath: options['gsp-path'] } : {})
1272
+ }
1273
+ const onWorker = options['no-worker'] !== true && workerRouteAvailable()
1274
+ let running
1275
+ try {
1276
+ running = onWorker
1277
+ ? await listenOnWorker(positional[0], serveOptions)
1278
+ : await listen(positional[0], serveOptions)
1279
+ } catch (error) {
1280
+ if (error instanceof ServeHostError && error.unknownOp) {
1281
+ return notWired('serve', error.message)
1282
+ }
1283
+ if (error instanceof Error && typeof error.message === 'string') {
1284
+ err(`factoidal serve: ${error.message.replace(/^l4factoidal:\s*/, '')}`)
1285
+ return EXIT_FAILURE
1286
+ }
1287
+ throw error
1288
+ }
1289
+ const info = onWorker ? running.info : running.served.info
1290
+ if (!quiet) {
1291
+ err(`serve: listening on ${running.origin} for ${positional[0]} ` +
1292
+ `(layout ${info.layout}, ${plural(info.rows, 'row')}, ` +
1293
+ `delta batches replayed: ${info.deltaBatches}, write scope ${info.writeScope})`)
1294
+ err(`serve: query ${running.origin}${info.queryPath}, graph store ${running.origin}${info.gspPath}`)
1295
+ }
1296
+ out(running.origin)
1297
+ return untilInterrupted(running, quiet)
1298
+ }
1299
+
1300
+ async function commandLwsServe (positional, options) {
1301
+ if (positional.length !== 1) throw new UsageError('lws-serve needs one DIR')
1302
+ const quiet = options.quiet === true
1303
+ const insecure = options['insecure-no-auth'] === true
1304
+ const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
1305
+ const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
1306
+ if (!insecure && issuerOption === null && authFile === null) {
1307
+ throw new UsageError('lws-serve needs --issuer IRI or --auth-config FILE, ' +
1308
+ 'or --insecure-no-auth for development')
1309
+ }
1310
+ if (insecure && (issuerOption !== null || authFile !== null)) {
1311
+ throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
1312
+ }
1313
+ let auth
1314
+ if (authFile !== null) {
1315
+ auth = readJsonFile(authFile)
1316
+ } else if (issuerOption !== null) {
1317
+ auth = { issuers: issuerOption.split(',').filter((v) => v !== '').map((issuer) => ({ issuer })) }
1318
+ }
1319
+ if (auth !== undefined && typeof options.audience === 'string') {
1320
+ auth = { ...auth, audience: options.audience }
1321
+ }
1322
+ const { listen, LwsHostError } = await import('../lws/server.mjs')
1323
+ let running
1324
+ try {
1325
+ running = await listen({
1326
+ port: servePort(options, 3000),
1327
+ host: serveHost(options),
1328
+ ...(typeof options.base === 'string' ? { baseIri: options.base } : {}),
1329
+ ...(insecure ? { insecureNoAuth: true } : { auth }),
1330
+ ...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
1331
+ ...(typeof options['root-acl'] === 'string'
1332
+ ? { rootAcl: new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options['root-acl'])) }
1333
+ : {}),
1334
+ realClock: true,
1335
+ dir: positional[0]
1336
+ })
1337
+ } catch (error) {
1338
+ if (error instanceof LwsHostError && error.unknownOp) {
1339
+ return notWired('lws-serve', error.message)
1340
+ }
1341
+ throw error
1342
+ }
1343
+ if (!quiet) {
1344
+ err(`lws-serve: listening on ${running.origin} for ${positional[0]}`)
1345
+ if (insecure) err('lws-serve: WARNING: --insecure-no-auth: every request is served without authorization')
1346
+ }
1347
+ out(running.origin)
1348
+ return untilInterrupted(running, quiet)
1349
+ }
1350
+
1351
+ async function commandSolidServe (positional, options) {
1352
+ if (positional.length !== 1) throw new UsageError('solid-serve needs one DIR')
1353
+ const quiet = options.quiet === true
1354
+ // The same rule as lws-serve: no start without a trusted issuer unless
1355
+ // the development mode is asked for by name.
1356
+ const insecure = options['insecure-no-auth'] === true
1357
+ const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
1358
+ const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
1359
+ if (!insecure && issuerOption === null && authFile === null) {
1360
+ throw new UsageError('solid-serve needs --issuer IRI or --auth-config FILE, ' +
1361
+ 'or --insecure-no-auth for development')
1362
+ }
1363
+ if (insecure && (issuerOption !== null || authFile !== null)) {
1364
+ throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
1365
+ }
1366
+ let auth
1367
+ if (authFile !== null) {
1368
+ auth = readJsonFile(authFile)
1369
+ } else if (issuerOption !== null) {
1370
+ auth = { issuers: issuerOption.split(',').filter((v) => v !== '').map((issuer) => ({ issuer })) }
1371
+ }
1372
+ const { listen, SolidServerHostError } = await import('../solid/server/index.mjs')
1373
+ let running
1374
+ try {
1375
+ running = await listen({
1376
+ port: servePort(options, 3000),
1377
+ host: serveHost(options),
1378
+ ...(typeof options.base === 'string' ? { baseIri: options.base } : {}),
1379
+ ...(insecure ? { insecureNoAuth: true } : { auth }),
1380
+ ...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
1381
+ realClock: true,
1382
+ dir: positional[0]
1383
+ })
1384
+ } catch (error) {
1385
+ if (error instanceof SolidServerHostError && error.unknownOp) {
1386
+ return notWired('solid-serve', error.message)
1387
+ }
1388
+ throw error
1389
+ }
1390
+ if (!quiet) {
1391
+ err(`solid-serve: listening on ${running.origin} for ${positional[0]}`)
1392
+ if (insecure) err('solid-serve: WARNING: --insecure-no-auth: every request is served without authentication')
1393
+ }
1394
+ out(running.origin)
1395
+ return untilInterrupted(running, quiet)
1396
+ }
1397
+
1398
+ // The command's verbs, as aliases for the wasm ABI's request kinds. A
1399
+ // kind name is accepted directly too, so this table adds a spelling and
1400
+ // hides nothing.
1401
+ const SOLID_CLIENT_ALIASES = {
1402
+ get: 'read',
1403
+ put: 'replace',
1404
+ post: 'create',
1405
+ delete: 'delete',
1406
+ discover: 'discoverStorage'
1407
+ }
1408
+
1409
+ async function commandSolidClient (positional, options) {
1410
+ if (positional.length !== 2) {
1411
+ throw new UsageError('solid-client needs an operation and a URL')
1412
+ }
1413
+ const [verb, target] = positional
1414
+ const { createSolidClient, SolidClientHostError, SOLID_REQUEST_KINDS } =
1415
+ await import('../solid/client/index.mjs')
1416
+ const kind = Object.prototype.hasOwnProperty.call(SOLID_CLIENT_ALIASES, verb)
1417
+ ? SOLID_CLIENT_ALIASES[verb]
1418
+ : verb
1419
+ if (!SOLID_REQUEST_KINDS.includes(kind)) {
1420
+ throw new UsageError(
1421
+ `unknown operation "${verb}"; one of ` +
1422
+ `${Object.keys(SOLID_CLIENT_ALIASES).join(', ')} or ` +
1423
+ `${SOLID_REQUEST_KINDS.join(', ')}`)
1424
+ }
1425
+ const args = { target }
1426
+ if (typeof options.file === 'string') {
1427
+ args.body = new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options.file))
1428
+ } else if (typeof options.body === 'string') {
1429
+ args.body = options.body
1430
+ }
1431
+ if (typeof options['content-type'] === 'string') {
1432
+ args.contentType = options['content-type']
1433
+ }
1434
+ if (['create', 'replace', 'patch'].includes(kind) && args.body === undefined) {
1435
+ throw new UsageError(`${verb} needs --body or --file`)
1436
+ }
1437
+ const extra = typeof options.interpret === 'string'
1438
+ ? { interpret: options.interpret }
1439
+ : {}
1440
+ let answer
1441
+ try {
1442
+ const client = await createSolidClient()
1443
+ answer = await client.run(kind, args, extra)
1444
+ } catch (error) {
1445
+ if (error instanceof SolidClientHostError && error.unknownOp) {
1446
+ return notWired('solid-client', error.message)
1447
+ }
1448
+ throw error
1449
+ }
1450
+ out(JSON.stringify(answer, null, 2))
1451
+ return EXIT_OK
1452
+ }
1453
+
909
1454
  const COMMANDS = {
910
1455
  version: (positional, options) => commandVersion(options),
911
- 'sample-store': (positional, options) => commandSampleStore(options),
1456
+ 'sample-store': (positional, options) => commandSampleStore(positional, options),
912
1457
  inspect: commandInspect,
913
1458
  query: commandQuery,
914
1459
  pack: commandPack,
915
1460
  activate: commandActivate,
916
1461
  update: commandUpdate,
917
- compact: commandCompact
1462
+ compact: commandCompact,
1463
+ serve: commandServe,
1464
+ 'lws-serve': commandLwsServe,
1465
+ 'solid-serve': commandSolidServe,
1466
+ 'solid-client': commandSolidClient
918
1467
  }
919
1468
 
920
1469
  // ----------------------------------------------------------------- main