eyeprolog 1.5.38 → 1.5.40

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/README.md CHANGED
@@ -58,11 +58,11 @@ printf 'human(socrates).\nmortal(X) :- human(X).\n' |
58
58
 
59
59
  ## Eyelet forward rules (`:+`)
60
60
  Normal mode executes top-level `Conclusion :+ Premise` rules through the bundled Prolog `library(eyelet)` driver when no explicit `-g/--goal` is supplied. `true :+ Goal` prints answers and `false :+ Goal` emits a `fuse/1`; JavaScript `run()` follows the same rule. The module exports `:+`, `stable/1`, and `becomes/2`, and implements rule selection, fixed-point rounds, skolemization, state replacement, and duplicate suppression in Prolog. JavaScript retains only syntax recognition, static dependency/autoload planning, driver bootstrap, and private mutability/output adapters; it does not implement the fixed-point semantics. Strict ISO mode disables this extension.
61
-
62
61
  Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL; use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
63
-
64
- All 33 bundled modules that overlap Scryer's current `src/lib/` surface cover Scryer's exported predicates, and all 26 bundled modules with public-module counterparts in Trealla's current `library/` surface cover those Trealla exports as well. The stricter Trealla/Scryer intersection remains a separate portability profile. Runtime-dependent library primitives follow one ownership rule: `src/lib/<module>.pl` calls private adapters from `src/<module>-host.js`; pure Prolog libraries need no host file. This includes the full Scryer `library(files)` surface, Scryer character-list paths in `library(pio)` with atom-path compatibility, chars/UTF-8/term/Base64 helpers, the completed arithmetic/time/`iso_ext` surfaces, the full Scryer `library(crypto)`, and Scryer-compatible TCP `library(sockets)`. Filesystem, OS, and TCP socket side effects require Node; crypto uses Node's backend where needed, with Web Crypto also used for secure random bytes when available. Non-integral rational conversions remain structural `rdiv(Numerator,Denominator)` terms until rational numbers become processor numeric values.
65
-
62
+ All 34 bundled modules that overlap Scryer's current library surface cover Scryer's exported predicates, and all 28 bundled modules with public-module counterparts in Trealla's current `library/` surface cover those Trealla exports as well. The stricter Trealla/Scryer intersection remains a separate portability profile. Runtime-dependent library primitives follow one ownership rule: `src/lib/<module>.pl` calls private adapters from `src/<module>-host.js`; pure Prolog libraries need no host file. This includes the full Scryer `library(files)` surface, Scryer character-list paths in `library(pio)` with atom-path compatibility, chars/UTF-8/term/Base64 helpers, the completed arithmetic/time/`iso_ext` surfaces, the full Scryer `library(crypto)`, Scryer-compatible TCP `library(sockets)`, the shared Scryer/Trealla `library(json)` DCG, and a merged Scryer/Trealla `library(http)` client/server facade. Filesystem, OS, TCP socket, and HTTP/HTTPS client side effects require Node; crypto uses Node's backend where needed, with Web Crypto also used for secure random bytes when available. Non-integral rational conversions remain structural `rdiv(Numerator,Denominator)` terms until rational numbers become processor numeric values.
63
+ ## HTTP and JSON
64
+ `library(http)` merges Scryer-style `http_open/3` options with Trealla's request helpers (`http_get/3`, `http_post/4`, `http_patch/4`, `http_put/4`, `http_delete/3`, `http_server/2`, `http_request/5`). HTTP/HTTPS clients use the Node-owned `src/http-host.js` bridge, follow up to five redirects, preserve repeated request-header values, accept Trealla `header(Name,Value)` and `host/path` address-list forms, and expose response bodies as UTF-8 character lists or streams. `http_open/3` reads response bodies lazily in bounded chunks rather than buffering the whole response through the host RPC bridge; request metadata and `data/1` still pass through that bounded bridge.
65
+ `library(json)` provides the shared bidirectional `json_chars//1` DCG with `pairs/1`, `list/1`, `string/1`, `number/1`, `boolean/1`, and `null`. JSON `\u` escapes combine and generate UTF-16 surrogate pairs for supplementary Unicode scalar values, while unpaired surrogates are rejected. See `examples/json.pl`, `examples/http-client.pl`, `test/run-http-json.mjs`, and the full contracts in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
66
66
  ## Links
67
67
 
68
68
  - [The Art of EyeProlog](https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog) — complete reference
@@ -0,0 +1,13 @@
1
+ % HTTP/HTTPS client example. This file has no automatic golden query because it
2
+ % intentionally depends on the network endpoint supplied by the caller.
3
+ %
4
+ % Example query:
5
+ % ?- fetch_json("https://example.org/api/status", JSON, Code).
6
+
7
+ :- use_module(library(http)).
8
+ :- use_module(library(json)).
9
+ :- use_module(library(dcgs)).
10
+
11
+ fetch_json(URL, JSON, Code) :-
12
+ http_get(URL, Body, [status_code(Code), header("accept", "application/json")]),
13
+ phrase(json_chars(JSON), Body).
@@ -0,0 +1,19 @@
1
+ % JSON parsing and generation with the shared Scryer/Trealla representation.
2
+ %
3
+ % Objects are pairs([...]), arrays are list([...]), strings are string(Chars),
4
+ % numbers are number(N), booleans are boolean(true/false), and null is null.
5
+ % Supplementary Unicode JSON escapes are combined from UTF-16 surrogate pairs.
6
+
7
+ :- use_module(library(json)).
8
+ :- use_module(library(dcgs)).
9
+
10
+ %% goal: json_example(Mode, Value)
11
+
12
+ json_example(parsed, JSON) :-
13
+ phrase(json_chars(JSON), "{\"name\":\"Ada\",\"active\":true,\"scores\":[3,5,8],\"emoji\":\"\\uD83D\\uDE00\"}").
14
+
15
+ json_example(generated, Chars) :-
16
+ once(phrase(json_chars(pairs([
17
+ string("project")-string("EyeProlog"),
18
+ string("ok")-boolean(true)
19
+ ])), Chars)).
File without changes
@@ -0,0 +1,2 @@
1
+ json_example(parsed, pairs([string("name") - string("Ada"), string("active") - boolean(true), string("scores") - list([number(3), number(5), number(8)]), string("emoji") - string("😀")])).
2
+ json_example(generated, "{\"project\":\"EyeProlog\",\"ok\":true}").
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.38",
6
+ "version": "1.5.40",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -74,6 +74,7 @@
74
74
  "conformance:update:wg17": "node tools/upgrade-wg17.mjs",
75
75
  "conformance:update:neumerkel": "node test/run-neumerkel.mjs --update-report",
76
76
  "conformance:check:neumerkel": "node test/run-neumerkel.mjs --cached --verify-report",
77
- "conformance:sync:neumerkel": "node test/run-neumerkel.mjs --cached --update-report"
77
+ "conformance:sync:neumerkel": "node test/run-neumerkel.mjs --cached --update-report",
78
+ "test:http-json": "node test/run-http-json.mjs"
78
79
  }
79
80
  }
package/playground.html CHANGED
@@ -551,6 +551,7 @@
551
551
  "herbrand-semantics",
552
552
  "herbrand-witnesses",
553
553
  "heron-theorem",
554
+ "http-client",
554
555
  "ideal-gas-law",
555
556
  "illegitimate-reasoning",
556
557
  "integer-partitions",
@@ -567,6 +568,7 @@
567
568
  "iso-reflective-terms",
568
569
  "iso-term-io",
569
570
  "job-shop-scheduling",
571
+ "json",
570
572
  "knapsack-optimization",
571
573
  "knowledge-engineering-alignment-flow",
572
574
  "knuth-bendix-completion",
@@ -101,3 +101,8 @@ runs in its own Node process, warms the engine before measured parse+execute
101
101
  runs, reports the median, and verifies a committed output SHA-256 before its
102
102
  timing is accepted. Machine-specific timing baselines live under `.benchmarks/`
103
103
  and are intentionally not versioned.
104
+
105
+
106
+ ## HTTP and JSON library ownership
107
+
108
+ `src/lib/json.pl` is a pure Prolog DCG adapted from the BSD-licensed Scryer/Trealla JSON library, with Unicode-scalar handling for JSON UTF-16 surrogate escapes. `src/lib/http.pl` owns HTTP option normalization, convenience predicates, request parsing, and the small server facade. Node HTTP/HTTPS client I/O is isolated in `src/http-host.js` and `src/http-worker.js`, following the same module-owned host-adapter convention as sockets, files, crypto, and other runtime-dependent libraries. The worker returns response metadata first and retains the response body behind a body id; the host stream pulls bounded chunks through the bridge on demand, so response size is no longer bounded by the RPC buffer and early `close/1` can discard the remaining transport body.
@@ -0,0 +1,159 @@
1
+ // Node HTTP(S) bridge for library(http). The Prolog module owns option parsing,
2
+ // response shaping, and server-side HTTP parsing; this host adapter performs the
3
+ // asynchronous client exchange and exposes each response body as a lazy stream.
4
+
5
+ import { isNode } from './platform.js';
6
+ import { PrologError } from './errors.js';
7
+ import {
8
+ ATOM, COMPOUND, NUMBER, STRING, VAR, atom, compound, copyResolved, deref,
9
+ listFromItems, numberTerm, properListItems, unify,
10
+ } from './term.js';
11
+ import { characterListText, chars } from './host-utils.js';
12
+
13
+ let WorkerCtor = null;
14
+ if (isNode) ({ Worker: WorkerCtor } = await import('node:worker_threads'));
15
+
16
+ const RPC_BYTES = 8 * 1024 * 1024;
17
+ const HTTP_READ_BYTES = 64 * 1024;
18
+ const HEADER_WORDS = 4;
19
+ let bridge = null;
20
+
21
+ class HttpBridge {
22
+ constructor() {
23
+ this.shared = new SharedArrayBuffer(HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT + RPC_BYTES);
24
+ this.header = new Int32Array(this.shared, 0, HEADER_WORDS);
25
+ this.bytes = new Uint8Array(this.shared, HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT);
26
+ this.encoder = new TextEncoder();
27
+ this.decoder = new TextDecoder();
28
+ this.worker = new WorkerCtor(new URL('./http-worker.js', import.meta.url), {
29
+ type: 'module', workerData: { shared: this.shared },
30
+ execArgv: typeof process !== 'undefined' ? process.execArgv.filter((arg) => !arg.startsWith('--input-type')) : [],
31
+ });
32
+ this.worker.unref();
33
+ const ready = Atomics.wait(this.header, 0, 0, 5000);
34
+ if (ready === 'timed-out' || Atomics.load(this.header, 0) !== -1) {
35
+ this.worker.terminate();
36
+ throw new PrologError('resource_error(http)');
37
+ }
38
+ Atomics.store(this.header, 0, 0);
39
+ }
40
+ rpc(request) {
41
+ const encoded = this.encoder.encode(JSON.stringify(request));
42
+ if (encoded.length > this.bytes.length) throw new PrologError('resource_error(http_message)');
43
+ this.bytes.set(encoded, 0);
44
+ Atomics.store(this.header, 1, encoded.length);
45
+ Atomics.store(this.header, 2, 0);
46
+ Atomics.store(this.header, 0, 1);
47
+ this.worker.postMessage(1);
48
+ Atomics.wait(this.header, 0, 1);
49
+ const responseLength = Atomics.load(this.header, 2);
50
+ const response = JSON.parse(this.decoder.decode(this.bytes.subarray(0, responseLength)));
51
+ Atomics.store(this.header, 0, 0);
52
+ if (!response.ok) {
53
+ if (response.error?.code === 'EPROTONOSUPPORT') throw new PrologError('domain_error(http_url_scheme)');
54
+ if (['ENOTFOUND', 'ECONNREFUSED', 'EHOSTUNREACH', 'ENETUNREACH'].includes(response.error?.code)) {
55
+ throw new PrologError('existence_error(source_sink)');
56
+ }
57
+ throw new PrologError('resource_error(http)');
58
+ }
59
+ return response.result;
60
+ }
61
+ }
62
+
63
+ function httpBridge() {
64
+ if (!isNode || WorkerCtor == null || typeof SharedArrayBuffer === 'undefined' || typeof Atomics?.wait !== 'function') {
65
+ throw new PrologError('resource_error(http)');
66
+ }
67
+ bridge ??= new HttpBridge();
68
+ return bridge;
69
+ }
70
+
71
+ function textValue(term, env, { allowNumber = false } = {}) {
72
+ const value = deref(term, env);
73
+ if (value.type === VAR) throw new PrologError('instantiation_error');
74
+ if (value.type === ATOM && value.name === '[]') return '';
75
+ if (value.type === ATOM || value.type === STRING || (allowNumber && value.type === NUMBER)) return value.name;
76
+ return characterListText(term, env);
77
+ }
78
+
79
+ function requestBodyValue(term, env) {
80
+ const value = deref(term, env);
81
+ if (value.type === ATOM && value.name === 'no_data') return null;
82
+ if (value.type === COMPOUND && value.name === 'data' && value.arity === 1) {
83
+ return textValue(value.args[0], env);
84
+ }
85
+ throw new PrologError('domain_error(http_data)', copyResolved(term, env));
86
+ }
87
+
88
+ function requestHeadersValue(term, env) {
89
+ const items = properListItems(deref(term, env), env);
90
+ if (items == null) throw new PrologError('type_error(list)', copyResolved(term, env));
91
+ return items.map((entryTerm) => {
92
+ const entry = deref(entryTerm, env);
93
+ if (entry.type !== COMPOUND || entry.name !== 'header' || entry.arity !== 2) {
94
+ throw new PrologError('domain_error(http_header)', copyResolved(entry, env));
95
+ }
96
+ return [textValue(entry.args[0], env), textValue(entry.args[1], env, { allowNumber: true })];
97
+ });
98
+ }
99
+
100
+ function streamHandle(id) { return compound('$stream', [numberTerm(id)]); }
101
+
102
+ function bytesFromBase64(text) {
103
+ return Uint8Array.from(Buffer.from(text, 'base64'));
104
+ }
105
+
106
+ function addBodyStream(solver, bodyId, finalUrl) {
107
+ const worker = httpBridge();
108
+ // Buffer.toString('utf8'), used by the original eager bridge, replaced malformed
109
+ // input with U+FFFD. Keep that behavior while decoding incrementally.
110
+ const decoder = new TextDecoder('utf-8');
111
+ const stream = {
112
+ id: solver.io.nextId++, alias: null, mode: 'read', type: 'text', content: '',
113
+ position: 0, reportedPosition: 0, path: String(finalUrl ?? ''), reposition: false,
114
+ eofAction: 'eof_code', standard: false, pastEnd: false, readable: true, writable: false,
115
+ remoteEnded: false, continuousRefill: true, httpBodyId: bodyId,
116
+ };
117
+ stream.interactiveReadUnit = () => {
118
+ while (true) {
119
+ const result = worker.rpc({ op: 'body_read', bodyId, maxBytes: HTTP_READ_BYTES });
120
+ if (result.eof) {
121
+ stream.remoteEnded = true;
122
+ const tail = decoder.decode();
123
+ return tail || null;
124
+ }
125
+ const text = decoder.decode(bytesFromBase64(result.data), { stream: true });
126
+ if (text.length > 0) return text;
127
+ }
128
+ };
129
+ stream.closeTransport = () => {
130
+ worker.rpc({ op: 'body_close', bodyId });
131
+ };
132
+ solver.io.add(stream);
133
+ return stream;
134
+ }
135
+
136
+ function* httpOpenBuiltin({ solver, goal, env }) {
137
+ const url = textValue(goal.args[0], env);
138
+ const method = textValue(goal.args[2], env);
139
+ const data = requestBodyValue(goal.args[3], env);
140
+ const requestHeaders = requestHeadersValue(goal.args[5], env);
141
+ const result = httpBridge().rpc({ op: 'request', url, method, data, headers: requestHeaders, redirects: 5 });
142
+ const stream = addBodyStream(solver, result.bodyId, result.finalUrl);
143
+ const headerTerms = result.headers.map(([name, value]) => compound('header', [atom(name), chars(value)]));
144
+ const next = env.clone();
145
+ if (unify(goal.args[1], streamHandle(stream.id), next) &&
146
+ unify(goal.args[4], numberTerm(result.statusCode), next) &&
147
+ unify(goal.args[6], listFromItems(headerTerms), next) &&
148
+ unify(goal.args[7], chars(result.finalUrl), next)) {
149
+ yield next;
150
+ } else {
151
+ solver.io.close(stream);
152
+ }
153
+ }
154
+
155
+ export const httpHostBuiltins = {
156
+ register(registry) {
157
+ registry.add('eyeprolog__http_open', 8, httpOpenBuiltin, { deterministic: true, eyePrologLibrary: true });
158
+ },
159
+ };
@@ -0,0 +1,170 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import { parentPort, workerData } from 'node:worker_threads';
4
+
5
+ const HEADER_WORDS = 4;
6
+ const header = new Int32Array(workerData.shared, 0, HEADER_WORDS);
7
+ const bytes = new Uint8Array(workerData.shared, HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT);
8
+ const decoder = new TextDecoder();
9
+ const encoder = new TextEncoder();
10
+ const bodies = new Map();
11
+ let nextBodyId = 1;
12
+
13
+ function errorRecord(error) {
14
+ return { code: String(error?.code ?? 'EUNKNOWN'), message: String(error?.message ?? error ?? 'http error') };
15
+ }
16
+
17
+ function writeResponse(response) {
18
+ const encoded = encoder.encode(JSON.stringify(response));
19
+ if (encoded.length > bytes.length) {
20
+ const fallback = encoder.encode(JSON.stringify({ ok: false, error: { code: 'EMSGSIZE', message: 'HTTP bridge message too large' } }));
21
+ bytes.set(fallback.subarray(0, bytes.length), 0);
22
+ Atomics.store(header, 2, Math.min(fallback.length, bytes.length));
23
+ } else {
24
+ bytes.set(encoded, 0);
25
+ Atomics.store(header, 2, encoded.length);
26
+ }
27
+ Atomics.store(header, 0, 2);
28
+ Atomics.notify(header, 0, 1);
29
+ }
30
+
31
+ function headerObject(requestHeaders) {
32
+ const headers = Object.create(null);
33
+ for (const [rawName, rawValue] of requestHeaders) {
34
+ const name = String(rawName).toLowerCase();
35
+ const value = String(rawValue);
36
+ const previous = headers[name];
37
+ if (previous == null) headers[name] = value;
38
+ else if (Array.isArray(previous)) previous.push(value);
39
+ else headers[name] = [previous, value];
40
+ }
41
+ return headers;
42
+ }
43
+
44
+ function responseHeaders(res) {
45
+ const rawHeaders = [];
46
+ for (let i = 0; i < res.rawHeaders.length; i += 2) {
47
+ rawHeaders.push([String(res.rawHeaders[i]).toLowerCase(), String(res.rawHeaders[i + 1] ?? '')]);
48
+ }
49
+ return rawHeaders;
50
+ }
51
+
52
+ function requestOnce(url, method, data, requestHeaders) {
53
+ return new Promise((resolve, reject) => {
54
+ const target = new URL(url);
55
+ const transport = target.protocol === 'https:' ? https : target.protocol === 'http:' ? http : null;
56
+ if (transport == null) {
57
+ reject(Object.assign(new Error(`unsupported URL scheme: ${target.protocol}`), { code: 'EPROTONOSUPPORT' }));
58
+ return;
59
+ }
60
+ const body = data == null ? null : Buffer.from(data, 'utf8');
61
+ const headers = headerObject(requestHeaders);
62
+ if (body != null && headers['content-length'] == null) headers['content-length'] = String(body.length);
63
+ const req = transport.request(target, { method: method.toUpperCase(), headers }, (res) => {
64
+ resolve({
65
+ response: res,
66
+ statusCode: Number(res.statusCode ?? 0),
67
+ headers: responseHeaders(res),
68
+ location: res.headers.location == null ? null : String(res.headers.location),
69
+ });
70
+ });
71
+ req.on('error', reject);
72
+ if (body != null && body.length > 0) req.write(body);
73
+ req.end();
74
+ });
75
+ }
76
+
77
+ function discardResponse(response) {
78
+ return new Promise((resolve) => {
79
+ response.on('error', resolve);
80
+ response.on('end', resolve);
81
+ response.resume();
82
+ });
83
+ }
84
+
85
+ function withoutContentLength(headers) {
86
+ return headers.filter(([name]) => String(name).toLowerCase() !== 'content-length');
87
+ }
88
+
89
+ async function requestFollowingRedirects(url, method, data, headers, redirects = 5) {
90
+ let current = url;
91
+ let currentMethod = method;
92
+ let currentData = data;
93
+ let currentHeaders = headers;
94
+ for (let count = 0; ; count++) {
95
+ const result = await requestOnce(current, currentMethod, currentData, currentHeaders);
96
+ if (![301, 302, 303, 307, 308].includes(result.statusCode) || result.location == null || count >= redirects) {
97
+ const bodyId = nextBodyId++;
98
+ bodies.set(bodyId, { response: result.response, iterator: result.response[Symbol.asyncIterator](), remainder: null });
99
+ return { statusCode: result.statusCode, headers: result.headers, bodyId, finalUrl: current };
100
+ }
101
+ await discardResponse(result.response);
102
+ current = new URL(result.location, current).toString();
103
+ if (result.statusCode === 303 || ((result.statusCode === 301 || result.statusCode === 302) && currentMethod.toLowerCase() === 'post')) {
104
+ currentMethod = 'get';
105
+ currentData = null;
106
+ currentHeaders = withoutContentLength(currentHeaders);
107
+ }
108
+ }
109
+ }
110
+
111
+ function encodeBodyChunk(state, chunk, maxBytes) {
112
+ const data = Buffer.from(chunk);
113
+ if (data.length <= maxBytes) return { data, remainder: null };
114
+ return { data: data.subarray(0, maxBytes), remainder: data.subarray(maxBytes) };
115
+ }
116
+
117
+ async function readBody(bodyId, maxBytes) {
118
+ const state = bodies.get(bodyId);
119
+ if (state == null) return { eof: true, data: '' };
120
+ const limit = Math.max(1, Math.min(Number(maxBytes) || 65536, 1024 * 1024));
121
+ if (state.remainder?.length) {
122
+ const { data, remainder } = encodeBodyChunk(state, state.remainder, limit);
123
+ state.remainder = remainder;
124
+ return { eof: false, data: data.toString('base64') };
125
+ }
126
+ const next = await state.iterator.next();
127
+ if (next.done) {
128
+ bodies.delete(bodyId);
129
+ return { eof: true, data: '' };
130
+ }
131
+ const { data, remainder } = encodeBodyChunk(state, next.value, limit);
132
+ state.remainder = remainder;
133
+ return { eof: false, data: data.toString('base64') };
134
+ }
135
+
136
+ function closeBody(bodyId) {
137
+ const state = bodies.get(bodyId);
138
+ if (state == null) return { closed: false };
139
+ bodies.delete(bodyId);
140
+ state.response.destroy();
141
+ return { closed: true };
142
+ }
143
+
144
+ async function dispatch(request) {
145
+ switch (request.op) {
146
+ case 'request':
147
+ return requestFollowingRedirects(request.url, request.method, request.data, request.headers, request.redirects ?? 5);
148
+ case 'body_read':
149
+ return readBody(request.bodyId, request.maxBytes);
150
+ case 'body_close':
151
+ return closeBody(request.bodyId);
152
+ default:
153
+ throw Object.assign(new Error(`unknown HTTP operation: ${request.op}`), { code: 'EINVAL' });
154
+ }
155
+ }
156
+
157
+ parentPort.on('message', async () => {
158
+ if (Atomics.load(header, 0) !== 1) return;
159
+ try {
160
+ const length = Atomics.load(header, 1);
161
+ const request = JSON.parse(decoder.decode(bytes.subarray(0, length)));
162
+ const result = await dispatch(request);
163
+ writeResponse({ ok: true, result });
164
+ } catch (error) {
165
+ writeResponse({ ok: false, error: errorRecord(error) });
166
+ }
167
+ });
168
+
169
+ Atomics.store(header, 0, -1);
170
+ Atomics.notify(header, 0, 1);
@@ -0,0 +1,220 @@
1
+ /** HTTP/HTTPS client helpers and a small HTTP server facade.
2
+
3
+ The client API combines the Scryer http_open/3 option surface with the
4
+ Trealla convenience predicates. HTTP and HTTPS client exchanges are
5
+ performed by the matching src/http-host.js adapter; response bodies are
6
+ returned as ordinary EyeProlog text streams.
7
+ */
8
+
9
+ :- module(http, [
10
+ http_open/3,
11
+ http_get/3,
12
+ http_post/4,
13
+ http_patch/4,
14
+ http_put/4,
15
+ http_delete/3,
16
+ http_server/2,
17
+ http_request/5
18
+ ]).
19
+
20
+ :- use_module(library(charsio), [get_line_to_chars/3, get_n_chars/3]).
21
+ :- use_module(library(error), [must_be/2]).
22
+ :- use_module(library(lists), [append/3, member/2, memberchk/2, maplist/3]).
23
+ :- use_module(library(sockets)).
24
+
25
+ :- meta_predicate(http_server(1, '?')).
26
+
27
+ http_open(Address0, Response, Options0) :-
28
+ must_be(list, Options0),
29
+ http__normalize_address(Address0, Options0, Address, Options),
30
+ http__method(Options, Method),
31
+ http__data_spec(Options, DataSpec),
32
+ http__request_headers(Options, RequestHeaders),
33
+ eyeprolog__http_open(Address, Response, Method, DataSpec, Code, RequestHeaders, RawHeaders, FinalUrl),
34
+ http__scryer_headers(RawHeaders, Headers),
35
+ http__bind_option(status_code, Code, Options),
36
+ http__bind_option(headers, Headers, Options),
37
+ http__bind_option(final_url, FinalUrl, Options),
38
+ http__bind_size(Options, RawHeaders).
39
+
40
+
41
+ http__normalize_address(Address, Options, URL, Effective) :-
42
+ nonvar(Address),
43
+ memberchk(host(Host), Address),
44
+ memberchk(path(Path), Address), !,
45
+ append(Address, Options, Effective),
46
+ ( memberchk(scheme(Scheme0), Address) -> Scheme = Scheme0
47
+ ; memberchk(https(true), Address) -> Scheme = https
48
+ ; Scheme = http
49
+ ),
50
+ http__text_chars(Scheme, SchemeChars),
51
+ http__text_chars(Host, HostChars),
52
+ http__text_chars(Path, Path0),
53
+ http__ensure_slash(Path0, PathChars),
54
+ ( memberchk(port(Port), Address) ->
55
+ http__text_chars(Port, PortChars),
56
+ append(HostChars, [':'|PortChars], Authority)
57
+ ; Authority = HostChars
58
+ ),
59
+ append(SchemeChars, "://", A0),
60
+ append(A0, Authority, A1),
61
+ append(A1, PathChars, URL).
62
+ http__normalize_address(Address, Options, Address, Options).
63
+
64
+ http__text_chars(Text, Chars) :- atom(Text), !, atom_chars(Text, Chars).
65
+ http__text_chars(Text, Chars) :- number(Text), !, number_chars(Text, Chars).
66
+ http__text_chars(Text, Text).
67
+
68
+ http__ensure_slash([], "/") :- !.
69
+ http__ensure_slash(['/'|Rest], ['/'|Rest]) :- !.
70
+ http__ensure_slash(Path, ['/'|Path]).
71
+
72
+ http_get(Address, Data, Options) :-
73
+ http__request_data(Address, get, no_data, Data, Options).
74
+
75
+ http_post(Address, PostData, Reply, Options) :-
76
+ http__checked_data(PostData, DataSpec),
77
+ http__request_data(Address, post, DataSpec, Reply, Options).
78
+
79
+ http_patch(Address, PostData, Reply, Options) :-
80
+ http__checked_data(PostData, DataSpec),
81
+ http__request_data(Address, patch, DataSpec, Reply, Options).
82
+
83
+ http_put(Address, PostData, Reply, Options) :-
84
+ http__checked_data(PostData, DataSpec),
85
+ http__request_data(Address, put, DataSpec, Reply, Options).
86
+
87
+ http_delete(Address, Data, Options) :-
88
+ http__request_data(Address, delete, no_data, Data, Options).
89
+
90
+ http__request_data(Address, DefaultMethod, Payload, Data, Options) :-
91
+ must_be(list, Options),
92
+ ( memberchk(method(Method0), Options) -> http__valid_method(Method0), Method = Method0 ; Method = DefaultMethod ),
93
+ ( memberchk(data(Data0), Options) -> http__checked_data(Data0, DataSpec) ; DataSpec = Payload ),
94
+ http__request_headers(Options, RequestHeaders),
95
+ eyeprolog__http_open(Address, Stream, Method, DataSpec, Code, RequestHeaders, RawHeaders, FinalUrl),
96
+ get_n_chars(Stream, _, Data),
97
+ close(Stream),
98
+ http__trealla_headers(RawHeaders, Headers),
99
+ http__bind_option(status_code, Code, Options),
100
+ http__bind_option(headers, Headers, Options),
101
+ http__bind_option(final_url, FinalUrl, Options),
102
+ http__bind_size(Options, RawHeaders).
103
+
104
+ http__method(Options, Method) :-
105
+ ( memberchk(method(Method0), Options) -> http__valid_method(Method0), Method = Method0 ; Method = get ).
106
+
107
+ http__valid_method(Method) :-
108
+ ( var(Method) -> throw(error(instantiation_error, http_open/3))
109
+ ; memberchk(Method, [get,post,put,delete,patch,head]) -> true
110
+ ; throw(error(domain_error(http_option, method(Method)), http_open/3))
111
+ ).
112
+
113
+ http__data_spec(Options, DataSpec) :-
114
+ ( memberchk(data(Data0), Options) -> http__checked_data(Data0, DataSpec)
115
+ ; DataSpec = no_data
116
+ ).
117
+
118
+ http__checked_data(Data, data(Data)) :-
119
+ ( var(Data) -> throw(error(instantiation_error, http_open/3)) ; true ).
120
+
121
+ http__request_headers(Options, Headers) :-
122
+ ( memberchk(request_headers(Input), Options) ->
123
+ ( var(Input) -> throw(error(instantiation_error, http_open/3)) ; true ),
124
+ maplist(http__header_term, Input, Base)
125
+ ; Base = [header('user-agent', "EyeProlog")]
126
+ ),
127
+ http__trealla_request_headers(Options, Extra),
128
+ append(Base, Extra, Headers).
129
+
130
+ http__trealla_request_headers([], []).
131
+ http__trealla_request_headers([header(Name,Value)|Options], [header(Name,Value)|Headers]) :- !,
132
+ http__trealla_request_headers(Options, Headers).
133
+ http__trealla_request_headers([_|Options], Headers) :-
134
+ http__trealla_request_headers(Options, Headers).
135
+
136
+ http__header_term(header(Name,Value), header(Name,Value)) :- !.
137
+ http__header_term(Name-Value, header(Name,Value)) :- !.
138
+ http__header_term(Name:Value, header(Name,Value)) :- !.
139
+ http__header_term(Term, header(Name,Value)) :-
140
+ Term =.. [Name,Value].
141
+
142
+ http__scryer_headers([], []).
143
+ http__scryer_headers([header(Name,Value)|Headers], [Term|Terms]) :-
144
+ Term =.. [Name,Value],
145
+ http__scryer_headers(Headers, Terms).
146
+
147
+ http__trealla_headers([], []).
148
+ http__trealla_headers([header(Name,Value)|Headers], [NameChars:Value|Terms]) :-
149
+ atom_chars(Name, NameChars),
150
+ http__trealla_headers(Headers, Terms).
151
+
152
+ http__bind_option(Name, Value, Options) :-
153
+ ( member(Option, Options), Option =.. [Name,Target] -> Target = Value ; true ).
154
+
155
+ http__bind_size(Options, RawHeaders) :-
156
+ ( memberchk(size(Size), Options) ->
157
+ ( memberchk(header('content-length',Chars), RawHeaders) -> number_chars(Size, Chars) ; fail )
158
+ ; true
159
+ ).
160
+
161
+ % One accepted connection per call. This mirrors Trealla's compact facade while
162
+ % keeping EyeProlog's synchronous execution model explicit.
163
+ http_server(Goal, Options) :-
164
+ must_be(list, Options),
165
+ ( memberchk(port(Port), Options) -> true ; Port = 0 ),
166
+ socket_server_open(Port, Server),
167
+ socket_server_accept(Server, _Client, Stream, [type(text)]),
168
+ socket_server_close(Server),
169
+ call(Goal, Stream).
170
+
171
+ http_request(Stream, Method, Path, Version, Headers) :-
172
+ http__read_line(Stream, RequestLine),
173
+ http__split_once(' ', RequestLine, Method0, Rest0),
174
+ http__split_once(' ', Rest0, Path, Version0),
175
+ http__uppercase(Method0, Method),
176
+ http__drop_http_prefix(Version0, Version),
177
+ http__read_headers(Stream, Headers).
178
+
179
+ http__read_headers(Stream, Headers) :-
180
+ http__read_line(Stream, Line),
181
+ ( Line = [] -> Headers = []
182
+ ; http__split_once(':', Line, Name0, Value0),
183
+ http__lowercase(Name0, Name),
184
+ http__trim_left(Value0, Value),
185
+ Headers = [Name:Value|Rest],
186
+ http__read_headers(Stream, Rest)
187
+ ).
188
+
189
+ http__read_line(Stream, Line) :-
190
+ get_line_to_chars(Stream, Raw, []),
191
+ http__strip_line_end(Raw, Line).
192
+
193
+ http__strip_line_end(Raw, Line) :-
194
+ ( append(Line0, ['\r','\n'], Raw) -> Line = Line0
195
+ ; append(Line0, ['\n'], Raw) -> Line = Line0
196
+ ; Line = Raw
197
+ ).
198
+
199
+ http__split_once(Sep, [Sep|Xs], [], Xs) :- !.
200
+ http__split_once(Sep, [X|Xs], [X|Ys], Rest) :-
201
+ http__split_once(Sep, Xs, Ys, Rest).
202
+
203
+ http__drop_http_prefix(['H','T','T','P','/'|Version], Version) :- !.
204
+ http__drop_http_prefix(Version, Version).
205
+
206
+ http__trim_left([' '|Xs], Ys) :- !, http__trim_left(Xs, Ys).
207
+ http__trim_left(['\t'|Xs], Ys) :- !, http__trim_left(Xs, Ys).
208
+ http__trim_left(Xs, Xs).
209
+
210
+ http__uppercase([], []).
211
+ http__uppercase([C|Cs], [U|Us]) :-
212
+ char_code(C, Code),
213
+ ( Code >= 97, Code =< 122 -> Upper is Code - 32, char_code(U, Upper) ; U = C ),
214
+ http__uppercase(Cs, Us).
215
+
216
+ http__lowercase([], []).
217
+ http__lowercase([C|Cs], [L|Ls]) :-
218
+ char_code(C, Code),
219
+ ( Code >= 65, Code =< 90 -> Lower is Code + 32, char_code(L, Lower) ; L = C ),
220
+ http__lowercase(Cs, Ls).