functionalscript 0.46.1 → 0.47.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/fjs/basen/cbase32/module.f.mjs +18 -10
- package/fjs/basen/cbase32/proof.f.d.mts +3 -0
- package/fjs/basen/cbase32/proof.f.mjs +24 -2
- package/fjs/basen/proof.f.mjs +8 -16
- package/fjs/bnf/ll1/proof.f.d.mts +2 -0
- package/fjs/bnf/ll1/proof.f.mjs +33 -18
- package/fjs/bnf/module.f.d.mts +2 -2
- package/fjs/bnf/module.f.mjs +2 -2
- package/fjs/cas/proof.f.mjs +4 -26
- package/fjs/ci/common/module.f.mjs +6 -0
- package/fjs/ci/config/module.f.d.mts +7 -7
- package/fjs/ci/config/module.f.mjs +7 -7
- package/fjs/cli/module.f.mjs +14 -10
- package/fjs/crypto/vdf/module.f.d.mts +0 -1
- package/fjs/crypto/vdf/module.f.mjs +3 -14
- package/fjs/dev/module.f.mjs +10 -20
- package/fjs/djs/parser/module.f.d.mts +101 -53
- package/fjs/djs/parser/module.f.mjs +732 -525
- package/fjs/djs/parser/proof.f.d.mts +6 -0
- package/fjs/djs/parser/proof.f.mjs +315 -11
- package/fjs/djs/parser/types.d.ts +29 -2
- package/fjs/djs/tokenizer/module.f.mjs +122 -105
- package/fjs/djs/tokenizer/proof.f.mjs +17 -0
- package/fjs/djs/transpiler/module.f.mjs +4 -5
- package/fjs/djs/transpiler/proof.f.d.mts +2 -0
- package/fjs/djs/transpiler/proof.f.mjs +25 -0
- package/fjs/edag/amnesia/module.f.d.mts +20 -0
- package/fjs/edag/amnesia/module.f.mjs +303 -0
- package/fjs/edag/amnesia/proof.f.d.mts +79 -0
- package/fjs/edag/amnesia/proof.f.mjs +573 -0
- package/fjs/edag/amnesia/types.d.ts +15 -0
- package/fjs/edag/module.f.d.mts +410 -0
- package/fjs/edag/module.f.mjs +559 -0
- package/fjs/edag/proof.f.d.mts +116 -0
- package/fjs/edag/proof.f.mjs +686 -0
- package/fjs/edag/types.d.ts +55 -0
- package/fjs/effects/node/memory/module.d.mts +37 -3
- package/fjs/effects/node/memory/module.mjs +38 -4
- package/fjs/effects/node/memory/proof.d.mts +5 -2
- package/fjs/effects/node/memory/proof.mjs +36 -6
- package/fjs/effects/node/module.d.mts +9 -1
- package/fjs/effects/node/module.f.d.mts +39 -0
- package/fjs/effects/node/module.f.mjs +46 -0
- package/fjs/effects/node/module.mjs +234 -32
- package/fjs/effects/node/types.d.ts +28 -2
- package/fjs/effects/node/virtual/module.f.d.mts +19 -10
- package/fjs/effects/node/virtual/module.f.mjs +246 -32
- package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
- package/fjs/effects/node/virtual/proof.f.mjs +232 -8
- package/fjs/effects/node/virtual/types.d.ts +57 -1
- package/fjs/emergent_testing/browser/proof.d.mts +55 -0
- package/fjs/emergent_testing/browser/proof.mjs +352 -0
- package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
- package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
- package/fjs/emergent_testing/browser.d.mts +79 -0
- package/fjs/emergent_testing/browser.mjs +432 -0
- package/fjs/emergent_testing/proof.f.d.mts +1 -1
- package/fjs/emergent_testing/proof.f.mjs +2 -2
- package/fjs/js/string_escape/module.f.d.mts +47 -0
- package/fjs/js/string_escape/module.f.mjs +95 -0
- package/fjs/js/string_escape/proof.f.d.mts +8 -0
- package/fjs/js/string_escape/proof.f.mjs +68 -0
- package/fjs/js/tokenizer/module.f.mjs +14 -12
- package/fjs/mcp/cas/module.f.d.mts +1 -7
- package/fjs/mcp/cas/module.f.mjs +24 -15
- package/fjs/mcp/evo/module.f.mjs +9 -24
- package/fjs/mcp/module.f.d.mts +3 -1
- package/fjs/mcp/module.f.mjs +4 -2
- package/fjs/media/json/rtti/module.f.d.mts +2 -2
- package/fjs/media/json/rtti/module.f.mjs +1 -1
- package/fjs/media/json/schema/module.f.d.mts +15 -5
- package/fjs/media/json/schema/module.f.mjs +42 -15
- package/fjs/media/json/schema/proof.f.d.mts +10 -1
- package/fjs/media/json/schema/proof.f.mjs +88 -16
- package/fjs/media/json/serializer/module.f.mjs +14 -26
- package/fjs/media/json/types.d.ts +1 -1
- package/fjs/media/lock/module.f.d.mts +8 -3
- package/fjs/media/lock/module.f.mjs +9 -3
- package/fjs/media/lock/proof.f.d.mts +1 -1
- package/fjs/media/lock/proof.f.mjs +2 -2
- package/fjs/media/lock/types.d.ts +1 -3
- package/fjs/media/module.f.d.mts +20 -13
- package/fjs/media/module.f.mjs +29 -16
- package/fjs/media/note/module.f.d.mts +14 -7
- package/fjs/media/note/module.f.mjs +14 -7
- package/fjs/media/note/proof.f.mjs +1 -1
- package/fjs/media/proof.f.mjs +5 -5
- package/fjs/media/revision/module.f.d.mts +8 -2
- package/fjs/media/revision/module.f.mjs +9 -3
- package/fjs/media/revision/proof.f.mjs +1 -1
- package/fjs/media/revision/types.d.ts +1 -1
- package/fjs/media/type/module.f.d.mts +20 -0
- package/fjs/media/type/module.f.mjs +74 -0
- package/fjs/media/type/proof.f.d.mts +5 -0
- package/fjs/media/type/proof.f.mjs +24 -1
- package/fjs/module.f.mjs +6 -0
- package/fjs/path/module.f.d.mts +60 -3
- package/fjs/path/module.f.mjs +138 -14
- package/fjs/path/proof.f.d.mts +3 -0
- package/fjs/path/proof.f.mjs +298 -2
- package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
- package/fjs/protocol/json_rpc/module.f.mjs +49 -13
- package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
- package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
- package/fjs/protocol/json_rpc/types.d.ts +12 -2
- package/fjs/protocol/mcp/module.f.d.mts +89 -54
- package/fjs/protocol/mcp/module.f.mjs +110 -56
- package/fjs/protocol/mcp/proof.f.d.mts +13 -0
- package/fjs/protocol/mcp/proof.f.mjs +107 -7
- package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
- package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
- package/fjs/protocol/mcp/types.d.ts +14 -2
- package/fjs/text/utf8/module.f.mjs +27 -1
- package/fjs/text/utf8/proof.f.mjs +37 -0
- package/fjs/types/array/module.f.mjs +18 -11
- package/fjs/types/array/proof.f.mjs +27 -1
- package/fjs/types/bigfloat/module.f.d.mts +48 -9
- package/fjs/types/bigfloat/module.f.mjs +147 -59
- package/fjs/types/bigfloat/proof.f.d.mts +2 -0
- package/fjs/types/bigfloat/proof.f.mjs +136 -4
- package/fjs/types/bigfloat/types.d.ts +17 -0
- package/fjs/types/function/module.f.d.mts +11 -2
- package/fjs/types/function/module.f.mjs +20 -2
- package/fjs/types/function/proof.f.mjs +5 -1
- package/fjs/types/phantom/types.d.ts +21 -0
- package/fjs/types/result/module.f.d.mts +4 -4
- package/fjs/types/result/module.f.mjs +2 -2
- package/fjs/types/result/proof.f.d.mts +6 -2
- package/fjs/types/result/proof.f.mjs +17 -1
- package/fjs/types/rtti/common/module.f.d.mts +78 -5
- package/fjs/types/rtti/common/module.f.mjs +168 -3
- package/fjs/types/rtti/common/proof.f.d.mts +14 -0
- package/fjs/types/rtti/common/proof.f.mjs +61 -2
- package/fjs/types/rtti/common/types.d.ts +23 -1
- package/fjs/types/rtti/data/module.f.d.mts +57 -1
- package/fjs/types/rtti/data/module.f.mjs +241 -47
- package/fjs/types/rtti/data/proof.f.d.mts +10 -0
- package/fjs/types/rtti/data/proof.f.mjs +272 -39
- package/fjs/types/rtti/data/types.d.ts +14 -7
- package/fjs/types/rtti/host.proof.d.mts +33 -0
- package/fjs/types/rtti/host.proof.mjs +170 -0
- package/fjs/types/rtti/module.f.d.mts +51 -6
- package/fjs/types/rtti/module.f.mjs +50 -3
- package/fjs/types/rtti/parse/module.f.d.mts +31 -24
- package/fjs/types/rtti/parse/module.f.mjs +197 -59
- package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
- package/fjs/types/rtti/parse/proof.f.mjs +171 -47
- package/fjs/types/rtti/proof.f.d.mts +6 -0
- package/fjs/types/rtti/proof.f.mjs +42 -1
- package/fjs/types/rtti/ts/module.f.d.mts +9 -3
- package/fjs/types/rtti/ts/module.f.mjs +68 -15
- package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
- package/fjs/types/rtti/ts/proof.f.mjs +139 -6
- package/fjs/types/rtti/ts/types.d.ts +164 -16
- package/fjs/types/rtti/types.d.ts +31 -3
- package/fjs/types/rtti/validate/module.f.d.mts +42 -25
- package/fjs/types/rtti/validate/module.f.mjs +187 -53
- package/fjs/types/rtti/validate/proof.f.d.mts +28 -2
- package/fjs/types/rtti/validate/proof.f.mjs +430 -75
- package/fjs/types/ts/types.d.ts +4 -0
- package/fjs/web/module.f.d.mts +115 -0
- package/fjs/web/module.f.mjs +581 -0
- package/fjs/web/proof.f.d.mts +39 -0
- package/fjs/web/proof.f.mjs +413 -0
- package/fjs/web/types.d.ts +39 -0
- package/fjs/website/browser-prepare.d.mts +5 -0
- package/fjs/website/browser-prepare.mjs +115 -0
- package/fjs/website/browser-source.d.mts +37 -0
- package/fjs/website/browser-source.mjs +202 -0
- package/fjs/website/browser-source.proof.d.mts +31 -0
- package/fjs/website/browser-source.proof.mjs +162 -0
- package/fjs/website/browser.d.mts +5 -0
- package/fjs/website/browser.mjs +8 -0
- package/fjs/website/module.f.d.mts +3 -3
- package/fjs/website/module.f.mjs +62 -9
- package/fjs/website/proof.f.d.mts +4 -0
- package/fjs/website/proof.f.mjs +18 -3
- package/package.json +2 -3
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* @import { IoResult, Server as EffectServer, Headers, Module, NodeOp, RequestListener as Erl, NodeProgram, NodeProgramOptions, WriteConsoles, TestContext, TestFn, } from './types.ts'
|
|
17
17
|
* @import { Result } from '../../types/result/types.ts'
|
|
18
18
|
* @import { StringMap } from '../../types/object/types.ts'
|
|
19
|
+
* @import { Nullable } from '../../types/nullable/types.ts'
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
import http from 'node:http'
|
|
@@ -30,14 +31,28 @@ import * as testContext from 'node:test'
|
|
|
30
31
|
import { concat, normalize, toPosix } from '../../path/module.f.mjs'
|
|
31
32
|
import { asyncRun } from '../module.mjs'
|
|
32
33
|
import { memoryOperationMap } from './memory/module.mjs'
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
emptyHost, emptyHostCode, emptyHostMessage, exitCode, toIoError, usesInlineTestContext,
|
|
36
|
+
} from './module.f.mjs'
|
|
34
37
|
import { asBase, asNominal } from '../../types/nominal/module.f.mjs'
|
|
35
38
|
import { error, ok, unwrap } from '../../types/result/module.f.mjs'
|
|
36
|
-
import { asyncTryCatch } from '../../types/result/module.mjs'
|
|
39
|
+
import { asyncTryCatch, tryCatch } from '../../types/result/module.mjs'
|
|
37
40
|
import { fromVec, listToVec, toVec } from '../../types/uint8array/module.f.mjs'
|
|
38
41
|
import { maxLengthBytes } from '../../types/bit_vec/module.f.mjs'
|
|
39
42
|
|
|
40
|
-
/**
|
|
43
|
+
/** The one thing this runner does with the socket a `connect` event hands it.
|
|
44
|
+
*
|
|
45
|
+
* @typedef {{ readonly end: (data: string) => void }} _Socket
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {{
|
|
50
|
+
* readonly listen: (port: number, host: string) => void,
|
|
51
|
+
* readonly once: (event: string, f: (e: unknown) => void) => void,
|
|
52
|
+
* on(event: string, f: (req: unknown, socket: _Socket) => void): void,
|
|
53
|
+
* readonly removeListener: (event: string, f: (e: unknown) => void) => void,
|
|
54
|
+
* }} _Server
|
|
55
|
+
*/
|
|
41
56
|
|
|
42
57
|
/** @typedef {AsyncIterable<Uint8Array>} _Readable */
|
|
43
58
|
|
|
@@ -53,6 +68,7 @@ import { maxLengthBytes } from '../../types/bit_vec/module.f.mjs'
|
|
|
53
68
|
* @typedef {{
|
|
54
69
|
* readonly writeHead: (status: number, headers: StringMap<string>) => _ServerResponse,
|
|
55
70
|
* readonly end: (body: Uint8Array) => void,
|
|
71
|
+
* readonly headersSent: boolean,
|
|
56
72
|
* }} _ServerResponse
|
|
57
73
|
*/
|
|
58
74
|
|
|
@@ -86,30 +102,146 @@ const io = async f => {
|
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
105
|
+
* Reads a request body, giving up at the `Vec` cap rather than at the point
|
|
106
|
+
* where converting it would throw.
|
|
107
|
+
*
|
|
108
|
+
* `listToVec` on an oversized body throws *after* the whole thing has been
|
|
109
|
+
* buffered, which is the wrong end of the problem twice over: the memory is
|
|
110
|
+
* already spent, and the throw lands inside an `async` request handler whose
|
|
111
|
+
* promise nobody awaits. Counting as the chunks arrive stops both.
|
|
112
|
+
*
|
|
113
|
+
* **The accumulator is mutated, deliberately.** Rebuilding the array per chunk
|
|
114
|
+
* — `result = [...result, a]`, the shape the rest of this repository is written
|
|
115
|
+
* in — copies every chunk received so far on every chunk received, which is
|
|
116
|
+
* quadratic in the *number* of chunks. The byte cap does not bound that: 20,000
|
|
117
|
+
* one-byte chunks are 20 KB and 200 million copies, and took 2,794 ms of event
|
|
118
|
+
* loop to reach an answer the server had already decided on — 167 ms now, and
|
|
119
|
+
* the growth went from ×4 per doubling to ×2. A cap on payload size is not a cap
|
|
120
|
+
* on chunk count, and a request that will be refused must not cost more than one
|
|
121
|
+
* that is served. The array never leaves this function before it
|
|
122
|
+
* is finished, so nothing observes the mutation — which is the condition under
|
|
123
|
+
* which the impure shell is allowed to be impure.
|
|
124
|
+
*
|
|
125
|
+
* @param {_Readable} v
|
|
126
|
+
* @returns {Promise<Nullable<readonly Uint8Array[]>>} `null` past the cap.
|
|
92
127
|
*/
|
|
93
|
-
const
|
|
94
|
-
/** @type {
|
|
95
|
-
|
|
128
|
+
const collectBounded = async v => {
|
|
129
|
+
/** @type {Uint8Array[]} */
|
|
130
|
+
const result = []
|
|
131
|
+
let size = 0
|
|
96
132
|
for await (const a of v) {
|
|
97
|
-
|
|
133
|
+
size += a.length
|
|
134
|
+
if (size > maxFileSizeBytes) { return null }
|
|
135
|
+
result.push(a)
|
|
98
136
|
}
|
|
99
137
|
return result
|
|
100
138
|
}
|
|
101
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The runner's own answer, for the cases a listener never gets to give one: a
|
|
142
|
+
* request body too large to hand it, and a listener that threw.
|
|
143
|
+
*
|
|
144
|
+
* **It closes the connection**, which is the whole difference between refusing
|
|
145
|
+
* a request and surviving the refusal. Both cases answer without having read
|
|
146
|
+
* the request to its end, and on a keep-alive connection Node then waits for
|
|
147
|
+
* the rest of a body that is never coming — the socket is stuck, and the next
|
|
148
|
+
* request on it is never answered. A client that declares ten megabytes and
|
|
149
|
+
* sends a hundred kilobytes could hold connections open that way for as long as
|
|
150
|
+
* it liked. Draining the remainder would be the polite alternative and the
|
|
151
|
+
* wrong one: it reads bytes this server has already decided it will not use.
|
|
152
|
+
*
|
|
153
|
+
* @type {(res: _ServerResponse) => (status: number) => (message: string) => void}
|
|
154
|
+
*/
|
|
155
|
+
const respondWith = res => status => message => {
|
|
156
|
+
const body = textEncoder.encode(`${message}\n`)
|
|
157
|
+
res
|
|
158
|
+
.writeHead(status, {
|
|
159
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
160
|
+
'content-length': `${body.length}`,
|
|
161
|
+
connection: 'close',
|
|
162
|
+
})
|
|
163
|
+
.end(body)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The whole HTTP response to a `CONNECT`, as bytes on a raw socket.
|
|
168
|
+
*
|
|
169
|
+
* Written by hand rather than through {@link respondWith}, because the `connect`
|
|
170
|
+
* event hands over a socket and not a `ServerResponse` — there is no object to
|
|
171
|
+
* ask for a status line.
|
|
172
|
+
*
|
|
173
|
+
* @type {string}
|
|
174
|
+
*/
|
|
175
|
+
const connectRefusal =
|
|
176
|
+
'HTTP/1.1 501 Not Implemented\r\n'
|
|
177
|
+
+ 'content-type: text/plain; charset=utf-8\r\n'
|
|
178
|
+
+ 'content-length: 26\r\n'
|
|
179
|
+
+ 'connection: close\r\n'
|
|
180
|
+
+ '\r\n'
|
|
181
|
+
+ 'this server cannot tunnel\n'
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Answers one request through `listener`, or explains that it could not.
|
|
185
|
+
*
|
|
186
|
+
* A body past the cap never reaches the listener: `IncomingMessage.body` is a
|
|
187
|
+
* single `Vec`, so there is no request value to build, and `413` is the accurate
|
|
188
|
+
* answer rather than a truncated one. Streaming bodies lift the whole limit —
|
|
189
|
+
* see `./todo/streaming-http-bodies.md`.
|
|
190
|
+
*
|
|
191
|
+
* `unwrap` is total here: a `RequestListener` answers
|
|
192
|
+
* `Effect<…, ServerResponse, never>`, because the response frame *is* where a
|
|
193
|
+
* listener puts its failures.
|
|
194
|
+
*
|
|
195
|
+
* @type {(listener: Erl<NodeOp>) => _RequestListener}
|
|
196
|
+
*/
|
|
197
|
+
const answerRequest = listener => async (req, res) => {
|
|
198
|
+
const body = await collectBounded(req)
|
|
199
|
+
if (body === null) {
|
|
200
|
+
respondWith(res)(413)('request body too large')
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
const { method, url, headers } = req
|
|
204
|
+
const { status, headers: outHeaders, body: outBody } = unwrap(await runNodeEffect(listener({
|
|
205
|
+
method,
|
|
206
|
+
url,
|
|
207
|
+
headers,
|
|
208
|
+
body: listToVec(body),
|
|
209
|
+
})))
|
|
210
|
+
res.writeHead(status, outHeaders).end(fromVec(outBody))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* What a request gets when answering it threw.
|
|
215
|
+
*
|
|
216
|
+
* Once the listener has started writing there is no status left to change, so
|
|
217
|
+
* the only thing owed is an end to the response — leaving it open would hang
|
|
218
|
+
* the connection until it times out.
|
|
219
|
+
*
|
|
220
|
+
* @type {(res: _ServerResponse) => void}
|
|
221
|
+
*/
|
|
222
|
+
const failSafe = res => {
|
|
223
|
+
if (res.headersSent) {
|
|
224
|
+
res.end(emptyBody)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
respondWith(res)(500)('internal server error')
|
|
228
|
+
}
|
|
229
|
+
|
|
102
230
|
const { mkdir, open, readFile, readdir, rename, writeFile, rm, access, stat } = fs.promises
|
|
103
231
|
|
|
104
232
|
const { exec } = childProcess
|
|
105
233
|
|
|
106
234
|
const maxFileSizeBytes = Number(maxLengthBytes)
|
|
107
235
|
|
|
236
|
+
const textEncoder = new TextEncoder()
|
|
237
|
+
|
|
238
|
+
const emptyBody = new Uint8Array()
|
|
239
|
+
|
|
108
240
|
const prefix = /** @type {const} */ ('file:///')
|
|
109
241
|
|
|
110
242
|
/** @type {(v: string) => Promise<Module>} */
|
|
111
243
|
const asyncImport = v => {
|
|
112
|
-
const s0 = v.includes(':')
|
|
244
|
+
const s0 = v.includes(':') ? v : concat(process.cwd())(v)
|
|
113
245
|
const s1 = s0.startsWith(prefix) ? s0 : `${prefix}${s0}`
|
|
114
246
|
return import(s1)
|
|
115
247
|
}
|
|
@@ -291,7 +423,10 @@ const runNodeEffect = asyncRun({
|
|
|
291
423
|
await fh.close()
|
|
292
424
|
}
|
|
293
425
|
}),
|
|
294
|
-
stat: path => io(async () =>
|
|
426
|
+
stat: path => io(async () => {
|
|
427
|
+
const s = await stat(path)
|
|
428
|
+
return { size: s.size, isFile: s.isFile() }
|
|
429
|
+
}),
|
|
295
430
|
import: path => io(() => asyncImport(path)),
|
|
296
431
|
exec: (command, stdin) => new Promise(resolve => {
|
|
297
432
|
const child = exec(command, (e, stdout, stderr) =>
|
|
@@ -300,31 +435,98 @@ const runNodeEffect = asyncRun({
|
|
|
300
435
|
child.stdin?.end(stdin)
|
|
301
436
|
}),
|
|
302
437
|
createServer: async requestListener => {
|
|
303
|
-
const
|
|
438
|
+
const answer = answerRequest(/** @type {Erl<NodeOp>} */ (requestListener))
|
|
439
|
+
// **Nothing may escape this handler.** Node does not await the promise
|
|
440
|
+
// an `async` request listener returns, so a throw inside one becomes an
|
|
441
|
+
// unhandled rejection and ends the *process*: one request, and the
|
|
442
|
+
// whole server is gone. A panic must not outlive the request that
|
|
443
|
+
// caused it, so the answer is caught here and the fallback — itself
|
|
444
|
+
// able to throw on a socket that has since died — is caught too.
|
|
304
445
|
/** @type {_RequestListener} */
|
|
305
446
|
const nodeRl = async (req, res) => {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
// `RequestListener` answers `Effect<…, ServerResponse, never>` —
|
|
309
|
-
// the response frame is where a listener puts its failures — so
|
|
310
|
-
// this unwrap is total.
|
|
311
|
-
const { status, headers: outHeaders, body: outBody } = unwrap(await runNodeEffect(erl({
|
|
312
|
-
method,
|
|
313
|
-
url,
|
|
314
|
-
headers,
|
|
315
|
-
body: listToVec(reqBody)
|
|
316
|
-
})))
|
|
317
|
-
res
|
|
318
|
-
.writeHead(status, outHeaders)
|
|
319
|
-
.end(fromVec(outBody))
|
|
447
|
+
const r = await asyncTryCatch(() => answer(req, res))
|
|
448
|
+
if (r[0] === 'error') { tryCatch(() => failSafe(res)) }
|
|
320
449
|
}
|
|
321
|
-
|
|
450
|
+
const server = createServer(nodeRl)
|
|
451
|
+
// A `CONNECT` never reaches the listener: Node routes it to the
|
|
452
|
+
// `connect` event, and with no handler there it drops the socket
|
|
453
|
+
// without a byte of HTTP — checked on Linux with Node 22.22.2, where
|
|
454
|
+
// `CONNECT localhost:18084 HTTP/1.1` closed the connection while a
|
|
455
|
+
// `POST` to the same server was answered. A client that asked a
|
|
456
|
+
// question deserves an answer, so the runner gives the one it can.
|
|
457
|
+
//
|
|
458
|
+
// `501`, not `405`. A `405` must carry `Allow` (RFC 9110 §15.5.6) and
|
|
459
|
+
// only the listener knows what it allows, while `501` is exactly what
|
|
460
|
+
// RFC 9110 §15.6.2 describes — a method the server cannot support for
|
|
461
|
+
// any resource. That is true of *every* server this effect layer can
|
|
462
|
+
// build: `RequestListener` maps a request frame to a response frame and
|
|
463
|
+
// has no vocabulary for a tunnel, so no listener could answer a
|
|
464
|
+
// `CONNECT` even if it were handed one.
|
|
465
|
+
//
|
|
466
|
+
// Answering here rather than passing it on follows the `413` and `500`
|
|
467
|
+
// above: the runner answers on the listener's behalf exactly when the
|
|
468
|
+
// listener structurally cannot.
|
|
469
|
+
server.on('connect', (_, socket) => { tryCatch(() => socket.end(connectRefusal)) })
|
|
470
|
+
return ok(/** @satisfies {EffectServer} */ (asNominal(server)))
|
|
322
471
|
},
|
|
323
|
-
|
|
472
|
+
// Binding is asynchronous, and its failure arrives as an `error` event
|
|
473
|
+
// rather than a throw: answering `ok` the moment `listen` was *called*
|
|
474
|
+
// reported a server that never started, and Node then killed the process
|
|
475
|
+
// with an unhandled `EADDRINUSE` — after the program had already printed
|
|
476
|
+
// the URL it was serving. So this settles on the outcome, not on the call.
|
|
477
|
+
listen: (server, port, host) => io(() => new Promise((resolve, reject) => {
|
|
324
478
|
const s = /** @type {_Server} */ (asBase(server))
|
|
325
|
-
s
|
|
326
|
-
|
|
327
|
-
|
|
479
|
+
// An empty host is the trap this operation's required `host` argument
|
|
480
|
+
// exists to close, so it is refused rather than forwarded. Node treats
|
|
481
|
+
// `''` exactly as it treats an omitted argument and binds the
|
|
482
|
+
// unspecified address — `0.0.0.0` on Linux with Node 22.22.2 and `::`
|
|
483
|
+
// on Darwin with Node 23.11.0, a different address each and the same
|
|
484
|
+
// mistake — which is how a missing configuration value publishes a
|
|
485
|
+
// server on every interface while the program believes it stated an
|
|
486
|
+
// address. A program that wants every interface says `'0.0.0.0'` or
|
|
487
|
+
// `'::'` and means it.
|
|
488
|
+
//
|
|
489
|
+
// The error is Node's own code and message shape for an argument it
|
|
490
|
+
// rejects, since a caller reading `IoError.code` should not have to
|
|
491
|
+
// learn a second vocabulary for a refusal that is this runner's own.
|
|
492
|
+
if (host === emptyHost) {
|
|
493
|
+
reject(Object.assign(new Error(emptyHostMessage), { code: emptyHostCode }))
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
// Each handler removes the other, so exactly one outcome is recorded and
|
|
497
|
+
// neither is left attached. `once` only removes the handler that fired:
|
|
498
|
+
// a failed bind used to leave its `listening` handler behind, and a
|
|
499
|
+
// caller retrying after an `EADDRINUSE` accumulated one per attempt
|
|
500
|
+
// until Node warned about the leak.
|
|
501
|
+
/** @type {() => void} */
|
|
502
|
+
const onListening = () => {
|
|
503
|
+
// A later `error` event is not this effect's to answer, and a
|
|
504
|
+
// handler still holding `reject` would swallow it into an
|
|
505
|
+
// already-settled promise.
|
|
506
|
+
s.removeListener('error', onError)
|
|
507
|
+
resolve(undefined)
|
|
508
|
+
}
|
|
509
|
+
/** @type {(e: unknown) => void} */
|
|
510
|
+
const onError = e => {
|
|
511
|
+
s.removeListener('listening', onListening)
|
|
512
|
+
reject(e)
|
|
513
|
+
}
|
|
514
|
+
s.once('error', onError)
|
|
515
|
+
s.once('listening', onListening)
|
|
516
|
+
// `listen` can also fail *synchronously* — an out-of-range port throws
|
|
517
|
+
// `ERR_SOCKET_BAD_PORT`, an already-listening server throws too — and a
|
|
518
|
+
// throw here would reject the promise past both handlers, leaving them
|
|
519
|
+
// attached: 20 attempts, 20 stale `error` handlers, each holding a
|
|
520
|
+
// `reject` that can never fire and would swallow a later error into an
|
|
521
|
+
// already-settled promise. So the synchronous path cleans up after
|
|
522
|
+
// itself, exactly as the two event paths do.
|
|
523
|
+
const started = tryCatch(() => s.listen(port, host))
|
|
524
|
+
if (started[0] === 'error') {
|
|
525
|
+
s.removeListener('error', onError)
|
|
526
|
+
s.removeListener('listening', onListening)
|
|
527
|
+
reject(started[1])
|
|
528
|
+
}
|
|
529
|
+
})),
|
|
328
530
|
forever: () => new Promise(() => {}),
|
|
329
531
|
now: async () => ok(now()),
|
|
330
532
|
sandbox: async f => ok(await sandbox(f)),
|
|
@@ -127,9 +127,20 @@ export type CreateExclusive = readonly ['createExclusive', (path: string) => IoR
|
|
|
127
127
|
export type WriteBytes = readonly ['writeBytes', (path: string, offset: number, data: Vec) => IoResult<void>];
|
|
128
128
|
/** @internal */
|
|
129
129
|
export type _WriteLoop = <O extends Operation>(offset: number, e: List<O, Vec, IoChannel>) => Effect<O | WriteBytes, void, IoChannel>;
|
|
130
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* File metadata returned by `stat`: the size in bytes, and whether the entry is
|
|
132
|
+
* a *regular* file.
|
|
133
|
+
*
|
|
134
|
+
* `isFile` is not a convenience. Reading a FIFO, a device or a socket is not
|
|
135
|
+
* reading a file: `open` on a FIFO with no writer blocks until one appears, so a
|
|
136
|
+
* `readFile` that reaches one never returns and holds a thread-pool slot for as
|
|
137
|
+
* long as it waits. Size cannot stand in for the check — a FIFO stats as zero
|
|
138
|
+
* bytes and passes every bound. It is the same question `Dirent` answers for a
|
|
139
|
+
* directory listing, asked about one path.
|
|
140
|
+
*/
|
|
131
141
|
export type FileStat = {
|
|
132
142
|
readonly size: number;
|
|
143
|
+
readonly isFile: boolean;
|
|
133
144
|
};
|
|
134
145
|
export type Stat = readonly ['stat', (path: string) => IoResult<FileStat>];
|
|
135
146
|
export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access | CreateExclusive | WriteBytes | Stat;
|
|
@@ -153,7 +164,22 @@ export type ServerResponse = {
|
|
|
153
164
|
*/
|
|
154
165
|
export type RequestListener<O extends Operation> = (_: IncomingMessage) => Effect<O, ServerResponse, never>;
|
|
155
166
|
export type CreateServer = ['createServer', (listener: RequestListener<Operation>) => OpResult<Server>];
|
|
156
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Starts accepting connections on `port` of `host`.
|
|
169
|
+
*
|
|
170
|
+
* The host is **required**, and that is the whole point of it: Node's own
|
|
171
|
+
* `listen(port)` binds the unspecified address, so omitting it publishes the
|
|
172
|
+
* server to every interface — a default nobody chose, and one a program serving
|
|
173
|
+
* local files must not get by writing less. Pass `'127.0.0.1'` for loopback
|
|
174
|
+
* only, `'0.0.0.0'` (or `'::'`) to accept from anywhere.
|
|
175
|
+
*
|
|
176
|
+
* It answers an {@link IoResult} because binding is where a server most often
|
|
177
|
+
* fails — the port is taken, the address is not the host's — and that failure
|
|
178
|
+
* arrives asynchronously, as the server's `error` event. An operation that
|
|
179
|
+
* answered the moment `listen` was *called* would report a server that never
|
|
180
|
+
* started, and leave the host to kill the process a moment later.
|
|
181
|
+
*/
|
|
182
|
+
export type Listen = ['listen', (server: Server, port: number, host: string) => IoResult<void>];
|
|
157
183
|
export type Http = CreateServer | Listen;
|
|
158
184
|
export type Forever = ['forever', () => OpResult<never>];
|
|
159
185
|
export type Module = StringMap<unknown>;
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @import { Vec } from '../../../types/bit_vec/types.ts'
|
|
7
7
|
* @import { PartialMemOperationMap, RunInstance } from '../../mock/types.ts'
|
|
8
|
-
* @import { Dirent, FileStat, IoError, IoResult, Module, NodeOp, NodeProgramOptions, SandboxResult } from '../types.ts'
|
|
8
|
+
* @import { Dirent, FileStat, IoError, IoResult, Module, NodeOp, NodeProgramOptions, OpResult, RequestListener, SandboxResult, Server } from '../types.ts'
|
|
9
|
+
* @import { Operation } from '../../types.ts'
|
|
9
10
|
* @import { Result } from '../../../types/result/types.ts'
|
|
10
11
|
* @import { Error } from '../../../types/result/types.ts'
|
|
11
|
-
* @import { Dir, State, _Entity } from './types.ts'
|
|
12
|
+
* @import { Dir, JsModule, State, _Entity, _VirtualListener, _VirtualServer } from './types.ts'
|
|
12
13
|
*/
|
|
13
14
|
import type { RunInstance } from '../../mock/types.ts';
|
|
14
15
|
import type { NodeOp, NodeProgramOptions } from '../types.ts';
|
|
@@ -18,14 +19,22 @@ export declare const emptyState: State;
|
|
|
18
19
|
/**
|
|
19
20
|
* The virtual runner.
|
|
20
21
|
*
|
|
21
|
-
* **It implements part of `NodeOp`, and says so.** `exec`, `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* **It implements part of `NodeOp`, and says so.** `exec`, `forever` and `test`
|
|
23
|
+
* have no meaning against an in-memory filesystem, and they used to be present
|
|
24
|
+
* as `todo` handlers — entries that existed only to satisfy a total operation
|
|
25
|
+
* map and threw when reached. They are simply absent now, so a program that
|
|
26
|
+
* asks for one gets `error(notImplemented)` back through its own continuation
|
|
27
|
+
* and decides what an incompatible runner means for it, which is what
|
|
28
|
+
* `NotImplemented` was introduced for. A command that is not a `NodeOp` at all
|
|
29
|
+
* still panics.
|
|
30
|
+
*
|
|
31
|
+
* `forever` is absent for a reason no implementation could remove: its result
|
|
32
|
+
* type is `Result<never, NotImplemented>`, so `error(notImplemented)` is the
|
|
33
|
+
* *only* value it can produce — a runner that cannot block forever has nothing
|
|
34
|
+
* else to answer, and a server program run here therefore ends where it would
|
|
35
|
+
* otherwise have blocked. `createServer` and `listen` do have meanings without
|
|
36
|
+
* a socket and are implemented above, which is what makes the rest of such a
|
|
37
|
+
* program provable.
|
|
29
38
|
*
|
|
30
39
|
* @type {RunInstance<NodeOp, State>}
|
|
31
40
|
*/
|