@factoidal/core 0.10.0 → 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.
- package/CHANGELOG.md +114 -1
- package/README.md +7 -1
- package/bin/ext.mjs +16 -20
- package/bin/factoidal.mjs +172 -69
- package/browser.d.ts +4 -0
- package/browser.js +4 -0
- package/fn.d.ts +1 -1
- package/fn.js +34 -97
- package/index.d.ts +22 -1
- package/index.mjs +4 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +155 -118
- package/lib/dataset.mjs +128 -34
- package/lib/lean-entry.mjs +23 -14
- package/lib/version.mjs +1 -1
- package/lws/README.md +46 -6
- package/lws/persist.mjs +78 -0
- package/lws/server.mjs +101 -16
- package/package.json +1 -1
- package/rdfjs.mjs +149 -226
- package/solid/client/index.mjs +25 -9
- package/solid/server/index.mjs +93 -15
package/lib/lean-entry.mjs
CHANGED
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
// None of them makes a semantic decision: which IRIs reach the host, what
|
|
23
23
|
// an absent answer means, which artifact a query reads, are decided in Lean.
|
|
24
24
|
|
|
25
|
-
const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
|
26
25
|
|
|
27
26
|
// The two npm entries and the engine each loads (issue 713 stage 4). The
|
|
28
27
|
// core engine's `ops` reflection lists what only the full engine serves
|
|
29
28
|
// (`fullOnlyOps`, `fullOnlyFormats`); a call to one of those names the
|
|
30
29
|
// entry that has it.
|
|
30
|
+
import { appTermToJson, termToJson } from '../rdfjs.mjs';
|
|
31
31
|
export const FULL_ENTRY = '@factoidal/core';
|
|
32
32
|
// The core profile is built for the hub pages only; the package ships the
|
|
33
33
|
// full engine (owner, 2026-10-20).
|
|
@@ -64,22 +64,22 @@ function failEnvelope(err) {
|
|
|
64
64
|
return JSON.stringify({ ok: false, error: String((err && err.message) || err) });
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// One
|
|
68
|
-
//
|
|
67
|
+
// One answer for Wasm/Ops/ExtFns.lean: the JavaScript value tagged with its
|
|
68
|
+
// JavaScript type ({js, v}; a number as its Number::toString text), or a term
|
|
69
|
+
// object. The engine chooses the datatype and the lexical form
|
|
70
|
+
// (L4Factoidal.XSD.JsValue.jsLiteral, issue 894 item 3). '' means "no
|
|
71
|
+
// value" (the section 17.6 error).
|
|
69
72
|
function encodeExtAnswer(out) {
|
|
70
73
|
if (out === null || out === undefined || out === EXT_PENDING_MARKER) return '';
|
|
71
|
-
|
|
72
|
-
return JSON.stringify({
|
|
74
|
+
switch (typeof out) {
|
|
75
|
+
case 'boolean': return JSON.stringify({ js: 'boolean', v: out });
|
|
76
|
+
case 'number': return JSON.stringify({ js: 'number', v: Object.is(out, -0) ? '-0' : String(out) });
|
|
77
|
+
case 'bigint': return JSON.stringify({ js: 'bigint', v: out.toString() });
|
|
78
|
+
case 'string': return JSON.stringify({ js: 'string', v: out });
|
|
79
|
+
case 'object':
|
|
80
|
+
try { return JSON.stringify(appTermToJson(out)); } catch (_error) { return ''; }
|
|
81
|
+
default: return '';
|
|
73
82
|
}
|
|
74
|
-
if (typeof out === 'number') {
|
|
75
|
-
if (!Number.isFinite(out)) return '';
|
|
76
|
-
return Number.isInteger(out)
|
|
77
|
-
? JSON.stringify({ type: 'literal', value: String(out), datatype: XSD + 'integer' })
|
|
78
|
-
: JSON.stringify({ type: 'literal', value: String(out), datatype: XSD + 'double' });
|
|
79
|
-
}
|
|
80
|
-
if (typeof out === 'string') return JSON.stringify({ type: 'literal', value: out });
|
|
81
|
-
if (typeof out === 'object' && typeof out.type === 'string') return JSON.stringify(out);
|
|
82
|
-
return '';
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// The Lean module asks its host through ONE global,
|
|
@@ -200,5 +200,14 @@ export function entryFromEngine(eng) {
|
|
|
200
200
|
}
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
|
+
// datasetQuadsBin answers a byte region (formal/lean4/L4Factoidal/RDF/QuadsBin.lean):
|
|
204
|
+
// this method answers {envelope, bytes} and throws on a refusal.
|
|
205
|
+
const blobIoOps = Array.isArray(reflection.blobIoOps) ? reflection.blobIoOps : [];
|
|
206
|
+
if (typeof eng.callBlobIO === 'function' && blobIoOps.includes('datasetQuadsBin')) {
|
|
207
|
+
entry.datasetQuadsBin = (handle, optsJson) =>
|
|
208
|
+
eng.callBlobIO('datasetQuadsBin', [String(handle), String(optsJson ?? '')], new Uint8Array(0));
|
|
209
|
+
} else {
|
|
210
|
+
delete entry.datasetQuadsBin;
|
|
211
|
+
}
|
|
203
212
|
return entry;
|
|
204
213
|
}
|
package/lib/version.mjs
CHANGED
package/lws/README.md
CHANGED
|
@@ -29,10 +29,13 @@ host or the engine failed rather than answered.
|
|
|
29
29
|
| `lwsOpen` | `[configJson]` | `{ ok, handle, root }` |
|
|
30
30
|
| `lwsStep` | `[handle, requestJson]` | `{ ok, response }` |
|
|
31
31
|
| `lwsClose` | `[handle]` | `{ ok }` |
|
|
32
|
+
| `lwsSnapshot` | `[handle]` | `{ ok, snapshot }` |
|
|
33
|
+
| `lwsFetched` | `[handle, documentJson]` | `{ ok, fetch? , installed?, refused? }` |
|
|
32
34
|
|
|
33
35
|
```
|
|
34
|
-
request { method, target, headers: [[name, value]]
|
|
35
|
-
response { status, headers: [[name, value]],
|
|
36
|
+
request { method, target, headers: [[name, value]] } + the content as bytes
|
|
37
|
+
response { status, headers: [[name, value]], bodyBytes } + the content as bytes
|
|
38
|
+
(through callBlobIO; https://github.com/danbri/factoidal/issues/889)
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
`headers` is a list of pairs, not an object: HTTP allows a field name to
|
|
@@ -43,7 +46,8 @@ unchanged and this host composes no IRI.
|
|
|
43
46
|
`configJson` members, all optional: `baseIri` (the absolute IRI the
|
|
44
47
|
storage root maps to), `now` (the engine clock in seconds, which advances
|
|
45
48
|
by one second per mutation so a write sequence has increasing
|
|
46
|
-
`Last-Modified` values with no real clock), `owner`, `agent
|
|
49
|
+
`Last-Modified` values with no real clock), `owner`, `agent`, `rootAcl`,
|
|
50
|
+
`auth` and `insecureNoAuth` (below). A request
|
|
47
51
|
may carry `now` and `agent` of its own to override the handle's for one
|
|
48
52
|
step; `listen({realClock: true})` is how a server with a real clock feeds
|
|
49
53
|
one in, and a test leaves it off to keep the engine's deterministic
|
|
@@ -55,12 +59,41 @@ A WebAssembly module built before these ops landed answers `unknown op`.
|
|
|
55
59
|
and every test in `tests/lws/` skips with the reason printed rather than
|
|
56
60
|
passing on an engine that cannot answer.
|
|
57
61
|
|
|
62
|
+
## Authorization
|
|
63
|
+
|
|
64
|
+
Authorization follows the draft's "Authorization" section
|
|
65
|
+
(https://github.com/danbri/factoidal/issues/886) and is decided by
|
|
66
|
+
`formal/lean4/L4Factoidal/LWS/Authorization.lean`. It is ON by default.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
const running = await listen({
|
|
70
|
+
port: 3000,
|
|
71
|
+
owner: 'https://id.example/alice', // root ACL: Read, Write, Control
|
|
72
|
+
auth: { issuers: [{ issuer: 'https://as.example' }] },
|
|
73
|
+
realClock: true
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
* Clients send `Authorization: Bearer <access token>` (RFC 6750). The
|
|
78
|
+
token is a JWT (RFC 9068) signed with ES256, RS256 or EdDSA by a trusted
|
|
79
|
+
issuer; `aud` must contain the storage IRI (`baseIri`, or `auth.audience`).
|
|
80
|
+
* Without a valid token the answer is 401 with
|
|
81
|
+
`WWW-Authenticate: Bearer resource_metadata="…"`; the RFC 9728 metadata
|
|
82
|
+
document is served at `/.well-known/oauth-protected-resource<path>`.
|
|
83
|
+
* The host fetches the issuer's `/.well-known/lws-configuration` and its
|
|
84
|
+
`jwks_uri` when the engine asks (`lwsStep` answers `{ fetch }`), passes
|
|
85
|
+
the bytes to `lwsFetched`, and steps again. `auth.issuers[].jwks` gives
|
|
86
|
+
the keys inline instead.
|
|
87
|
+
* `insecureNoAuth: true` serves every request with no authorization. It is
|
|
88
|
+
for development only, and must be asked for.
|
|
89
|
+
|
|
58
90
|
## Use
|
|
59
91
|
|
|
60
92
|
```js
|
|
61
93
|
import { listen } from '@factoidal/core/lws'
|
|
62
94
|
|
|
63
|
-
const running = await listen({ port: 3000, baseIri: 'https://storage.example/'
|
|
95
|
+
const running = await listen({ port: 3000, baseIri: 'https://storage.example/',
|
|
96
|
+
insecureNoAuth: true }) // development only; see Authorization
|
|
64
97
|
console.log(running.origin)
|
|
65
98
|
await running.close()
|
|
66
99
|
```
|
|
@@ -68,9 +101,13 @@ await running.close()
|
|
|
68
101
|
From the command:
|
|
69
102
|
|
|
70
103
|
```
|
|
71
|
-
factoidal lws-serve /var/lib/lws --port 3000
|
|
104
|
+
factoidal lws-serve /var/lib/lws --port 3000 --issuer https://as.example --owner https://id.example/alice
|
|
105
|
+
factoidal lws-serve /tmp/dev --port 3000 --insecure-no-auth
|
|
72
106
|
```
|
|
73
107
|
|
|
108
|
+
One of `--issuer`, `--auth-config FILE` or `--insecure-no-auth` is
|
|
109
|
+
required.
|
|
110
|
+
|
|
74
111
|
`--port 0` takes an ephemeral port and the command prints the one it got;
|
|
75
112
|
`--base IRI` sets `baseIri`, which defaults to the bound origin. `DIR`
|
|
76
113
|
names the storage directory, but the first slice keeps the resource tree
|
|
@@ -86,12 +123,15 @@ in the engine handle, so nothing is read from it or written to it yet
|
|
|
86
123
|
| `openLwsStorage(options)` | open a handle and get `step` / `close` |
|
|
87
124
|
| `createLwsServer(...).setBaseIri(iri)` | set `baseIri` before the first request |
|
|
88
125
|
| `lwsOpsAvailable(engine)` | probe the loaded module for the ops |
|
|
89
|
-
| `LWS_OPS` | the
|
|
126
|
+
| `LWS_OPS` | the op names |
|
|
90
127
|
| `LwsHostError` | a host failure, with `unknownOp` set when the op is absent |
|
|
91
128
|
|
|
92
129
|
## Tests
|
|
93
130
|
|
|
94
131
|
```
|
|
95
132
|
node tests/lws/server.mjs
|
|
133
|
+
node tests/lws/draft-binding.mjs
|
|
134
|
+
node tests/lws/binary-bodies.mjs
|
|
135
|
+
node tests/lws/authorization.mjs
|
|
96
136
|
deno run --allow-read --allow-net --allow-write --allow-env tests/lws/server.mjs
|
|
97
137
|
```
|
package/lws/persist.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Durable storage for `factoidal lws-serve DIR` and `solid-serve DIR`.
|
|
2
|
+
// https://github.com/danbri/factoidal/issues/659
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS FILE IS ALLOWED TO DO
|
|
5
|
+
// Move bytes between one file and the engine. The engine decides what
|
|
6
|
+
// the bytes are (`L4Factoidal/LWS/Snapshot.lean`): the snapshot document,
|
|
7
|
+
// which paths it may hold (no `..`, no absolute or encoded traversal), and
|
|
8
|
+
// when the storage changed (the `changed` member of the step envelope).
|
|
9
|
+
// This file decides only where the one file lives and how it is written:
|
|
10
|
+
//
|
|
11
|
+
// DIR/lws-state.json the snapshot, replaced atomically
|
|
12
|
+
//
|
|
13
|
+
// No request target ever becomes a file name here, so a request cannot
|
|
14
|
+
// address a file outside DIR. The write is: a temporary file in DIR,
|
|
15
|
+
// fsync, rename over the state file, fsync of DIR. A crash leaves the old
|
|
16
|
+
// snapshot or the new one, never a torn one.
|
|
17
|
+
|
|
18
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeSync, existsSync } from 'node:fs'
|
|
19
|
+
import { join, resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
export const STATE_FILE = 'lws-state.json'
|
|
22
|
+
|
|
23
|
+
/** The absolute path of the state file of a storage directory. */
|
|
24
|
+
export function stateFileOf (dir) {
|
|
25
|
+
return join(resolve(dir), STATE_FILE)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The parsed snapshot in DIR, or `null` when DIR holds none yet. The
|
|
30
|
+
* engine checks the document; a file that does not parse as JSON is an
|
|
31
|
+
* error of this host, reported as such.
|
|
32
|
+
*/
|
|
33
|
+
export function readSnapshot (dir) {
|
|
34
|
+
const file = stateFileOf(dir)
|
|
35
|
+
if (!existsSync(file)) return null
|
|
36
|
+
const text = readFileSync(file, 'utf8')
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`${file} is not JSON: ${error.message}`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Write a snapshot document to DIR, atomically. */
|
|
45
|
+
export function writeSnapshot (dir, snapshot) {
|
|
46
|
+
const root = resolve(dir)
|
|
47
|
+
mkdirSync(root, { recursive: true })
|
|
48
|
+
const file = stateFileOf(root)
|
|
49
|
+
const temporary = join(root, `.${STATE_FILE}.${process.pid}.tmp`)
|
|
50
|
+
const fd = openSync(temporary, 'w', 0o600)
|
|
51
|
+
try {
|
|
52
|
+
writeSync(fd, JSON.stringify(snapshot))
|
|
53
|
+
fsyncSync(fd)
|
|
54
|
+
} finally {
|
|
55
|
+
closeSync(fd)
|
|
56
|
+
}
|
|
57
|
+
renameSync(temporary, file)
|
|
58
|
+
try {
|
|
59
|
+
const dirFd = openSync(root, 'r')
|
|
60
|
+
try { fsyncSync(dirFd) } finally { closeSync(dirFd) }
|
|
61
|
+
} catch (_error) {
|
|
62
|
+
// Some platforms cannot fsync a directory; the rename stands.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* After one step: when the engine says the storage changed, ask it for
|
|
68
|
+
* the snapshot (`snapshotOp`) and write it.
|
|
69
|
+
*/
|
|
70
|
+
export function persistIfChanged (engine, snapshotOp, handle, envelope, dir) {
|
|
71
|
+
if (typeof dir !== 'string' || envelope === null || typeof envelope !== 'object') return
|
|
72
|
+
if (envelope.changed !== true) return
|
|
73
|
+
const answer = engine.call(snapshotOp, [handle])
|
|
74
|
+
if (answer === null || typeof answer !== 'object' || answer.snapshot === undefined) {
|
|
75
|
+
throw new Error(`${snapshotOp} answered with no snapshot`)
|
|
76
|
+
}
|
|
77
|
+
writeSnapshot(dir, answer.snapshot)
|
|
78
|
+
}
|
package/lws/server.mjs
CHANGED
|
@@ -21,12 +21,25 @@
|
|
|
21
21
|
// lwsOpen [configJson] -> { ok, handle, root }
|
|
22
22
|
// lwsStep [handle, requestJson] -> { ok, response }
|
|
23
23
|
// lwsClose [handle] -> { ok }
|
|
24
|
+
// lwsFetched [handle, documentJson] -> { ok, fetch? | installed? | refused? }
|
|
24
25
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
26
|
+
// `lwsStep` may answer `{ ok, fetch: { issuer, url } }` instead of a
|
|
27
|
+
// response: the engine needs an authorization server's metadata or key
|
|
28
|
+
// set before it can decide. This host fetches that URL, passes
|
|
29
|
+
// `{ issuer, url, status, body, now }` to `lwsFetched`, follows any
|
|
30
|
+
// further `fetch` it names, and steps the request again.
|
|
27
31
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
32
|
+
// request { method, target, headers: [[name, value]] } + content bytes
|
|
33
|
+
// response { status, headers: [[name, value]], bodyBytes } + content bytes
|
|
34
|
+
//
|
|
35
|
+
// Bodies are BYTES (https://github.com/danbri/factoidal/issues/889). The
|
|
36
|
+
// step goes through the engine's byte-region entry, `callBlobIO`: the
|
|
37
|
+
// request content is the IN region and the response content is the OUT
|
|
38
|
+
// region, with no encoding and no decoding here. This host never reads a
|
|
39
|
+
// body as text.
|
|
40
|
+
//
|
|
41
|
+
// The config members are `baseIri`, `now`, `owner`, `agent`, `rootAcl`,
|
|
42
|
+
// `auth` and `insecureNoAuth`, all optional. `target` is a path within the storage, so this host passes
|
|
30
43
|
// Node's `request.url` through unchanged and never composes an absolute
|
|
31
44
|
// IRI: `baseIri` tells the engine what the root maps to.
|
|
32
45
|
//
|
|
@@ -48,9 +61,30 @@
|
|
|
48
61
|
// produced and this code keeps working unchanged.
|
|
49
62
|
|
|
50
63
|
import { loadEngine } from '../bin/engine.mjs'
|
|
64
|
+
import { readSnapshot, persistIfChanged } from './persist.mjs'
|
|
51
65
|
|
|
52
66
|
/** The ops this host needs from the engine. */
|
|
53
|
-
export const LWS_OPS = ['lwsOpen', 'lwsStep', 'lwsClose']
|
|
67
|
+
export const LWS_OPS = ['lwsOpen', 'lwsStep', 'lwsClose', 'lwsSnapshot', 'lwsFetched']
|
|
68
|
+
|
|
69
|
+
/** The most documents one request may make this host fetch before the
|
|
70
|
+
* engine has answered it. The engine names at most two per issuer (its
|
|
71
|
+
* metadata, then its key set); this bound only stops a defective module
|
|
72
|
+
* from looping the host. */
|
|
73
|
+
const MAX_FETCHES = 8
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fetch one document the engine named, for `lwsFetched`. Pure I/O: the
|
|
77
|
+
* engine decides what the bytes mean. A network failure is reported as
|
|
78
|
+
* status 0, which the engine refuses like any other failed fetch.
|
|
79
|
+
*/
|
|
80
|
+
async function fetchDocument (url) {
|
|
81
|
+
try {
|
|
82
|
+
const answer = await fetch(url, { redirect: 'error' })
|
|
83
|
+
return { status: answer.status, body: await answer.text() }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { status: 0, body: String(error && error.message ? error.message : error) }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
54
88
|
|
|
55
89
|
/** An error raised by the LWS host, as opposed to one the engine
|
|
56
90
|
* reported. `unknownOp` is set when the loaded WebAssembly module does
|
|
@@ -108,7 +142,7 @@ function handleOf (envelope) {
|
|
|
108
142
|
return null
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
function responseOf (envelope) {
|
|
145
|
+
function responseOf (envelope, bytes) {
|
|
112
146
|
if (envelope === null || typeof envelope !== 'object') {
|
|
113
147
|
throw new LwsHostError('lwsStep answered with no envelope')
|
|
114
148
|
}
|
|
@@ -119,10 +153,18 @@ function responseOf (envelope) {
|
|
|
119
153
|
throw new LwsHostError('lwsStep answered with no status in its response record')
|
|
120
154
|
}
|
|
121
155
|
const headers = Array.isArray(record.headers) ? record.headers : []
|
|
122
|
-
const body =
|
|
156
|
+
const body = bytes instanceof Uint8Array ? bytes : new Uint8Array(0)
|
|
123
157
|
return { status: record.status, headers, body }
|
|
124
158
|
}
|
|
125
159
|
|
|
160
|
+
/** The request content as bytes: a byte array is passed as it is, and a
|
|
161
|
+
* string (a JavaScript caller's convenience) as its UTF-8 bytes. */
|
|
162
|
+
function contentBytes (request) {
|
|
163
|
+
if (request.body instanceof Uint8Array) return request.body
|
|
164
|
+
if (typeof request.body === 'string') return new TextEncoder().encode(request.body)
|
|
165
|
+
return new Uint8Array(0)
|
|
166
|
+
}
|
|
167
|
+
|
|
126
168
|
/**
|
|
127
169
|
* Open one LWS storage on the engine and return the handle plus the two
|
|
128
170
|
* calls a host makes against it.
|
|
@@ -146,6 +188,20 @@ export async function openLwsStorage (options = {}) {
|
|
|
146
188
|
if (typeof options.now === 'number') config.now = options.now
|
|
147
189
|
if (typeof options.owner === 'string') config.owner = options.owner
|
|
148
190
|
if (typeof options.agent === 'string') config.agent = options.agent
|
|
191
|
+
// Authorization (L4Factoidal/LWS/Authorization.lean). `auth` is passed
|
|
192
|
+
// through unread; `insecureNoAuth` is the explicit development mode.
|
|
193
|
+
// With neither, the engine enforces authorization with no trusted
|
|
194
|
+
// issuer.
|
|
195
|
+
if (options.auth !== undefined && options.auth !== null) config.auth = options.auth
|
|
196
|
+
if (options.insecureNoAuth === true) config.insecureNoAuth = true
|
|
197
|
+
if (typeof options.rootAcl === 'string') config.rootAcl = options.rootAcl
|
|
198
|
+
// `dir` names the storage directory. The snapshot found there, if any,
|
|
199
|
+
// is the storage to continue from; the engine checks it.
|
|
200
|
+
const dir = typeof options.dir === 'string' ? options.dir : null
|
|
201
|
+
if (dir !== null) {
|
|
202
|
+
const snapshot = readSnapshot(dir)
|
|
203
|
+
if (snapshot !== null) config.snapshot = snapshot
|
|
204
|
+
}
|
|
149
205
|
let opened
|
|
150
206
|
try {
|
|
151
207
|
opened = engine.call('lwsOpen', [JSON.stringify(config)])
|
|
@@ -167,14 +223,43 @@ export async function openLwsStorage (options = {}) {
|
|
|
167
223
|
/**
|
|
168
224
|
* One protocol step.
|
|
169
225
|
* @param {{method: string, target: string,
|
|
170
|
-
* headers: Array<[string, string]>, body: string}} request
|
|
171
|
-
* @returns {{status: number, headers: Array<[string, string]>, body:
|
|
226
|
+
* headers: Array<[string, string]>, body: Uint8Array|string}} request
|
|
227
|
+
* @returns {{status: number, headers: Array<[string, string]>, body: Uint8Array}}
|
|
172
228
|
*/
|
|
173
|
-
step (request) {
|
|
229
|
+
async step (request) {
|
|
230
|
+
const content = contentBytes(request)
|
|
231
|
+
const { body: _body, ...fields } = request
|
|
174
232
|
const record = options.realClock === true
|
|
175
|
-
? { ...
|
|
176
|
-
:
|
|
177
|
-
|
|
233
|
+
? { ...fields, now: Math.floor(Date.now() / 1000) }
|
|
234
|
+
: fields
|
|
235
|
+
const callStep = () => engine.callBlobIO('lwsStep', [handle, JSON.stringify(record)], content)
|
|
236
|
+
let answered = callStep()
|
|
237
|
+
let envelope = answered.envelope
|
|
238
|
+
// The engine may answer "fetch this document first" (an issuer's
|
|
239
|
+
// metadata or key set). The host fetches exactly what it is told,
|
|
240
|
+
// hands the bytes back, and steps the request again.
|
|
241
|
+
let fetches = 0
|
|
242
|
+
while (envelope !== null && typeof envelope === 'object' &&
|
|
243
|
+
envelope.fetch !== undefined && envelope.fetch !== null) {
|
|
244
|
+
let wanted = envelope.fetch
|
|
245
|
+
while (wanted !== undefined && wanted !== null) {
|
|
246
|
+
fetches += 1
|
|
247
|
+
if (fetches > MAX_FETCHES) {
|
|
248
|
+
throw new LwsHostError('the engine asked for more documents than one request may fetch')
|
|
249
|
+
}
|
|
250
|
+
const got = await fetchDocument(wanted.url)
|
|
251
|
+
const told = engine.call('lwsFetched', [handle, JSON.stringify({
|
|
252
|
+
issuer: wanted.issuer, url: wanted.url, status: got.status, body: got.body,
|
|
253
|
+
...(typeof record.now === 'number' ? { now: record.now } : {})
|
|
254
|
+
})])
|
|
255
|
+
wanted = told !== null && typeof told === 'object' ? told.fetch : null
|
|
256
|
+
}
|
|
257
|
+
answered = callStep()
|
|
258
|
+
envelope = answered.envelope
|
|
259
|
+
}
|
|
260
|
+
const answer = responseOf(envelope, answered.bytes)
|
|
261
|
+
if (dir !== null) persistIfChanged(engine, 'lwsSnapshot', handle, envelope, dir)
|
|
262
|
+
return answer
|
|
178
263
|
},
|
|
179
264
|
close () {
|
|
180
265
|
try { engine.call('lwsClose', [handle]) } catch (_error) { /* already gone */ }
|
|
@@ -182,13 +267,13 @@ export async function openLwsStorage (options = {}) {
|
|
|
182
267
|
}
|
|
183
268
|
}
|
|
184
269
|
|
|
185
|
-
/** Read a Node request body as
|
|
270
|
+
/** Read a Node request body as its bytes. */
|
|
186
271
|
function readBody (request) {
|
|
187
272
|
return new Promise((resolve, reject) => {
|
|
188
273
|
const chunks = []
|
|
189
274
|
request.on('data', (chunk) => chunks.push(chunk))
|
|
190
275
|
request.on('error', reject)
|
|
191
|
-
request.on('end', () => resolve(Buffer.concat(chunks)
|
|
276
|
+
request.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))))
|
|
192
277
|
})
|
|
193
278
|
}
|
|
194
279
|
|
|
@@ -250,7 +335,7 @@ export async function createLwsServer (options = {}) {
|
|
|
250
335
|
}
|
|
251
336
|
let answer
|
|
252
337
|
try {
|
|
253
|
-
answer = (await storage()).step(record)
|
|
338
|
+
answer = await (await storage()).step(record)
|
|
254
339
|
} catch (error) {
|
|
255
340
|
// A failure of this host or of the engine, not a protocol answer.
|
|
256
341
|
// The engine's own words are kept; nothing is invented.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factoidal/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "RDF parsing and writing (Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD), SPARQL 1.1 and 1.2, RDFS and OWL reasoning, SHACL and ShEx validation, CSVW and RML mapping, RDFC-1.0, and SPARQL, LWS and Solid servers, for Node, Deno and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sparql",
|