functionalscript 0.46.0 → 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 +120 -0
- package/fjs/types/rtti/validate/module.f.mjs +351 -0
- package/fjs/types/rtti/validate/proof.f.d.mts +154 -0
- package/fjs/types/rtti/validate/proof.f.mjs +781 -0
- 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
|
@@ -5,20 +5,22 @@
|
|
|
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
|
|
|
14
15
|
import { assert, todo } from '../../../asserts/module.f.mjs'
|
|
15
16
|
import { isProperPrefix, join, parse } from '../../../path/module.f.mjs'
|
|
16
17
|
import { utf8ToString } from '../../../text/module.f.mjs'
|
|
17
18
|
import { empty, length, maxLengthBytes, msb, vec } from '../../../types/bit_vec/module.f.mjs'
|
|
18
|
-
import { error, ok } from '../../../types/result/module.f.mjs'
|
|
19
|
-
import { ioError, nodeCommands } from '../module.f.mjs'
|
|
19
|
+
import { error, ok, unwrap } from '../../../types/result/module.f.mjs'
|
|
20
|
+
import { emptyHost, emptyHostError, ioError, nodeCommands } from '../module.f.mjs'
|
|
20
21
|
import { partialRun } from '../../mock/module.f.mjs'
|
|
21
22
|
import { asBase, asNominal } from '../../memory/module.f.mjs'
|
|
23
|
+
import { asBase as asBaseServer, asNominal as asNominalServer } from '../../../types/nominal/module.f.mjs'
|
|
22
24
|
|
|
23
25
|
/** @type {State} */
|
|
24
26
|
export const emptyState = {
|
|
@@ -31,8 +33,29 @@ export const emptyState = {
|
|
|
31
33
|
memoryNext: 0,
|
|
32
34
|
memoryValues: {},
|
|
33
35
|
randomNext: 0,
|
|
36
|
+
listening: [],
|
|
37
|
+
requests: [],
|
|
38
|
+
responses: [],
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @param {_Entity} entity
|
|
43
|
+
* @returns {entity is readonly Vec[]}
|
|
44
|
+
*/
|
|
45
|
+
const isBinFile = entity => Array.isArray(entity)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {_Entity} entity
|
|
49
|
+
* @returns {entity is JsModule}
|
|
50
|
+
*/
|
|
51
|
+
const isJsModule = entity => typeof entity === 'function'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {_Entity} entity
|
|
55
|
+
* @returns {entity is Dir}
|
|
56
|
+
*/
|
|
57
|
+
const isDir = entity => !isBinFile(entity) && !isJsModule(entity)
|
|
58
|
+
|
|
36
59
|
/**
|
|
37
60
|
* @template T
|
|
38
61
|
* @param {(dir: Dir, path: readonly string[]) => readonly [Dir, T]} op
|
|
@@ -46,10 +69,7 @@ const operation = op => {
|
|
|
46
69
|
}
|
|
47
70
|
const [first, ...rest] = path
|
|
48
71
|
const subDir = dir[first]
|
|
49
|
-
|
|
50
|
-
// which `readonly Vec[]` is not assignable to, so its negative branch never
|
|
51
|
-
// removes a `readonly` array from `_Entity`. Only `instanceof` narrows here.
|
|
52
|
-
if (typeof subDir !== 'object' || subDir instanceof Array) {
|
|
72
|
+
if (subDir === undefined || !isDir(subDir)) {
|
|
53
73
|
return op(dir, path)
|
|
54
74
|
}
|
|
55
75
|
const [newSubDir, r] = f(subDir, rest)
|
|
@@ -105,12 +125,12 @@ const enoent = error(ioError({ code: 'ENOENT', message: 'no such file or directo
|
|
|
105
125
|
const readFile = readOperation((dir, path) => {
|
|
106
126
|
if (path.length !== 1) { return enoent }
|
|
107
127
|
const file = dir[path[0]]
|
|
108
|
-
if (typeof file === 'function') { throw new Error(`'${path[0]}' is a JsModule; readFile not supported`) }
|
|
109
128
|
if (file === undefined) { return enoent }
|
|
129
|
+
if (isJsModule(file)) { throw new Error(`'${path[0]}' is a JsModule; readFile not supported`) }
|
|
110
130
|
// `operation`'s wrapper descends into every plain-object (`Dir`) entry
|
|
111
131
|
// before this op ever runs, and the `JsModule` case already threw above,
|
|
112
132
|
// so `file` here is always a `Vec[]` — never a bare `Dir`.
|
|
113
|
-
assert(
|
|
133
|
+
assert(isBinFile(file), `'${path[0]}' is not a file`)
|
|
114
134
|
const chunks = file
|
|
115
135
|
const capBits = maxLengthBytes * 8n
|
|
116
136
|
let result = empty
|
|
@@ -129,7 +149,7 @@ const readFile = readOperation((dir, path) => {
|
|
|
129
149
|
const import_ = readOperation((dir, path) => {
|
|
130
150
|
if (path.length !== 1) { return fail('no such file') }
|
|
131
151
|
const entry = dir[path[0]]
|
|
132
|
-
if (
|
|
152
|
+
if (entry === undefined || !isJsModule(entry)) { return fail(`'${path[0]}' is not a JsModule`) }
|
|
133
153
|
return ok(entry())
|
|
134
154
|
})
|
|
135
155
|
|
|
@@ -140,7 +160,7 @@ const writeFileOp = payload => (dir, path) => {
|
|
|
140
160
|
if (path.length !== 1) { return [dir, writeFileError] }
|
|
141
161
|
const [name] = path
|
|
142
162
|
const file = dir[name]
|
|
143
|
-
if (file !== undefined && !
|
|
163
|
+
if (file !== undefined && !isBinFile(file)) { return [dir, writeFileError] }
|
|
144
164
|
dir = { ...dir, [name]: [payload] }
|
|
145
165
|
return [dir, okVoid]
|
|
146
166
|
}
|
|
@@ -161,7 +181,7 @@ const readdir = (base, recursive) => readOperation((dir, path) => {
|
|
|
161
181
|
let result = []
|
|
162
182
|
for (const [name, content] of entries(d)) {
|
|
163
183
|
if (content === undefined) { continue }
|
|
164
|
-
const isFile = content
|
|
184
|
+
const isFile = !isDir(content)
|
|
165
185
|
result = [...result, { name, parentPath, isFile }]
|
|
166
186
|
if (!isFile && recursive) {
|
|
167
187
|
result = [...result, ...f(join(parentPath, name), content)]
|
|
@@ -209,7 +229,7 @@ const extractEntity = (dir, path) => {
|
|
|
209
229
|
}
|
|
210
230
|
const [first, ...rest] = path
|
|
211
231
|
const sub = dir[first]
|
|
212
|
-
if (sub === undefined || sub
|
|
232
|
+
if (sub === undefined || !isDir(sub)) { return [dir, enoent] }
|
|
213
233
|
const [newSub, result] = extractEntity(sub, rest)
|
|
214
234
|
if (result[0] === 'error') { return [dir, result] }
|
|
215
235
|
return [{ ...dir, [first]: newSub }, result]
|
|
@@ -228,8 +248,8 @@ const insertEntityAt = (dir, path, entity) => {
|
|
|
228
248
|
const [name] = path
|
|
229
249
|
const existing = dir[name]
|
|
230
250
|
if (existing !== undefined) {
|
|
231
|
-
const entityIsDir =
|
|
232
|
-
const existingIsDir =
|
|
251
|
+
const entityIsDir = isDir(entity)
|
|
252
|
+
const existingIsDir = isDir(existing)
|
|
233
253
|
if (entityIsDir && !existingIsDir) {
|
|
234
254
|
return [dir, fail(`cannot overwrite file '${name}' with a directory`)]
|
|
235
255
|
}
|
|
@@ -249,7 +269,7 @@ const insertEntityAt = (dir, path, entity) => {
|
|
|
249
269
|
const [first, ...rest] = path
|
|
250
270
|
const sub = dir[first]
|
|
251
271
|
if (sub === undefined) { return [dir, enoent] }
|
|
252
|
-
if (sub
|
|
272
|
+
if (!isDir(sub)) { return [dir, fail('not a directory')] }
|
|
253
273
|
const [newSub, result] = insertEntityAt(sub, rest, entity)
|
|
254
274
|
if (result[0] === 'error') { return [dir, result] }
|
|
255
275
|
return [{ ...dir, [first]: newSub }, result]
|
|
@@ -276,12 +296,12 @@ const rename = (src, dst) => state => {
|
|
|
276
296
|
const readBytesOp = (path, offset, size) => readOperation((dir, p) => {
|
|
277
297
|
if (p.length !== 1) { return enoent }
|
|
278
298
|
const file = dir[p[0]]
|
|
279
|
-
if (typeof file === 'function') { throw new Error(`'${p[0]}' is a JsModule; readBytes not supported`) }
|
|
280
299
|
if (file === undefined) { return enoent }
|
|
300
|
+
if (isJsModule(file)) { throw new Error(`'${p[0]}' is a JsModule; readBytes not supported`) }
|
|
281
301
|
// `operation`'s wrapper descends into every plain-object (`Dir`) entry
|
|
282
302
|
// before this op ever runs, and the `JsModule` case already threw above,
|
|
283
303
|
// so `file` here is always a `Vec[]` — never a bare `Dir`.
|
|
284
|
-
assert(
|
|
304
|
+
assert(isBinFile(file), `'${p[0]}' is not a file`)
|
|
285
305
|
if (!Number.isInteger(offset)) { return fail(`Offset ${offset} is not an integer`) }
|
|
286
306
|
if (!Number.isInteger(size)) { return fail(`Chunk size ${size} is not an integer`) }
|
|
287
307
|
if (offset < 0) { return fail(`Offset ${offset} is negative`) }
|
|
@@ -306,6 +326,12 @@ const readBytesOp = (path, offset, size) => readOperation((dir, p) => {
|
|
|
306
326
|
return ok(result)
|
|
307
327
|
})(path)
|
|
308
328
|
|
|
329
|
+
/** What `stat` answers for a name that exists and is not a regular file.
|
|
330
|
+
*
|
|
331
|
+
* @type {IoResult<FileStat>}
|
|
332
|
+
*/
|
|
333
|
+
const notRegular = ok({ size: 0, isFile: false })
|
|
334
|
+
|
|
309
335
|
/** Total byte size of a chunk-list file (each chunk is byte-aligned).
|
|
310
336
|
*
|
|
311
337
|
* @type {(chunks: readonly Vec[]) => number}
|
|
@@ -339,7 +365,7 @@ const writeBytesRawOp = (offset, data) => (dir, p) => {
|
|
|
339
365
|
const [name] = p
|
|
340
366
|
const file = dir[name]
|
|
341
367
|
if (file === undefined) { return [dir, enoent] } // writeBytes never creates
|
|
342
|
-
if (!
|
|
368
|
+
if (!isBinFile(file)) { return [dir, fail(`'${name}' is not a file`)] }
|
|
343
369
|
if (!Number.isInteger(offset) || offset < 0) { return [dir, fail(`Offset ${offset} is invalid`)] }
|
|
344
370
|
const chunks = file
|
|
345
371
|
if (offset !== fileSizeBytes(chunks)) {
|
|
@@ -351,15 +377,193 @@ const writeBytesRawOp = (offset, data) => (dir, p) => {
|
|
|
351
377
|
/** @type {(path: string, offset: number, data: Vec) => (state: State) => readonly [State, IoResult<void>]} */
|
|
352
378
|
const writeBytesOp = (path, offset, data) => operation(writeBytesRawOp(offset, data))(path)
|
|
353
379
|
|
|
354
|
-
/**
|
|
355
|
-
|
|
380
|
+
/**
|
|
381
|
+
* `stat` reports what is there, including when what is there is not a regular
|
|
382
|
+
* file — a host stats a directory or a FIFO successfully and says it is not a
|
|
383
|
+
* file, and a caller's guard against reading one can only be exercised here if
|
|
384
|
+
* this runner does the same.
|
|
385
|
+
*
|
|
386
|
+
* Two entries answer `isFile: false`. A `JsModule` is this file system's stand-in
|
|
387
|
+
* for a name that exists and is not a file at all. A **directory** arrives as an
|
|
388
|
+
* empty remaining path, because `operation` has already descended into it — the
|
|
389
|
+
* one way to reach `statOp` with nothing left to look up — and that is what it
|
|
390
|
+
* means, root included.
|
|
391
|
+
*
|
|
392
|
+
* @type {(path: string) => (state: State) => readonly [State, IoResult<FileStat>]}
|
|
393
|
+
*/
|
|
394
|
+
const statPath = readOperation((dir, path) => {
|
|
395
|
+
if (path.length === 0) { return notRegular }
|
|
356
396
|
if (path.length !== 1) { return enoent }
|
|
357
397
|
const file = dir[path[0]]
|
|
358
398
|
if (file === undefined) { return enoent }
|
|
359
|
-
|
|
360
|
-
|
|
399
|
+
// `isBinFile` rather than a local `Array.isArray`: which entity kind a name
|
|
400
|
+
// holds is asked in one place now (#1697), and `stat` is one of its askers.
|
|
401
|
+
if (!isBinFile(file)) { return notRegular }
|
|
402
|
+
return ok({ size: fileSizeBytes(file), isFile: true })
|
|
361
403
|
})
|
|
362
404
|
|
|
405
|
+
/**
|
|
406
|
+
* An empty path names nothing, and `parse` cannot say so: it collapses to the
|
|
407
|
+
* same empty segment list `.` does, and `.` is the root. A host answers `ENOENT`
|
|
408
|
+
* for `stat('')`, so this asks the question `parse` has already thrown away —
|
|
409
|
+
* before the answer can depend on it.
|
|
410
|
+
*
|
|
411
|
+
* @type {(path: string) => (state: State) => readonly [State, IoResult<FileStat>]}
|
|
412
|
+
*/
|
|
413
|
+
const statOp = path => path === '' ? state => [state, enoent] : statPath(path)
|
|
414
|
+
|
|
415
|
+
// ── HTTP ──────────────────────────────────────────────────────────────────────
|
|
416
|
+
//
|
|
417
|
+
// There are no sockets here, but the two operations that *set up* a server still
|
|
418
|
+
// have a faithful meaning without one: `createServer` hands back a handle, and
|
|
419
|
+
// `listen` accepts exactly the requests the fixture queued. That is what makes a
|
|
420
|
+
// request-in / response-out proof possible against the in-memory filesystem —
|
|
421
|
+
// the same listener the Node runner would drive, driven by fixture data instead
|
|
422
|
+
// of a socket. `forever` is the one HTTP-adjacent operation with no meaning
|
|
423
|
+
// here; see the note on {@link virtual} below.
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Whether `port` is one a host would accept: an integer in `0`–`65535`, where
|
|
427
|
+
* `0` asks for an ephemeral one. Node throws `ERR_SOCKET_BAD_PORT` for anything
|
|
428
|
+
* else, and a runner that accepted `-1` or `NaN` would let a program be proven
|
|
429
|
+
* that cannot run.
|
|
430
|
+
*
|
|
431
|
+
* @type {(port: number) => boolean}
|
|
432
|
+
*/
|
|
433
|
+
const isPort = port => Number.isInteger(port) && port >= 0 && port <= maxPort
|
|
434
|
+
|
|
435
|
+
/** @type {number} */
|
|
436
|
+
const maxPort = 0xffff
|
|
437
|
+
|
|
438
|
+
/** The port that asks for any free port rather than naming one.
|
|
439
|
+
*
|
|
440
|
+
* @type {number}
|
|
441
|
+
*/
|
|
442
|
+
const ephemeral = 0
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The handle **is** the listener.
|
|
446
|
+
*
|
|
447
|
+
* `Server` is a nominal over `unknown`, so what a runner keeps inside one is its
|
|
448
|
+
* own business: this one keeps the listener there, and `listen` takes it back
|
|
449
|
+
* out. In the state instead, it would model a runner with room for one server —
|
|
450
|
+
* two `createServer` calls and the second silently answers for the first, where
|
|
451
|
+
* the Node runner gives each its own socket — so it rides in the handle, and two
|
|
452
|
+
* servers in one program are two servers here too.
|
|
453
|
+
*
|
|
454
|
+
* A fresh record wraps it for that last reason: two calls with the *same*
|
|
455
|
+
* listener must still be two servers, and a handle that was the listener itself
|
|
456
|
+
* made them one — which `listen` then read as a single server listening twice.
|
|
457
|
+
*
|
|
458
|
+
* @type {(listener: RequestListener<Operation>) => (state: State) => readonly[State, OpResult<Server>]}
|
|
459
|
+
*/
|
|
460
|
+
const createServer = listener => state => {
|
|
461
|
+
// The same narrowing the Node runner performs before handing a request to
|
|
462
|
+
// it: `CreateServer` declares its listener over `Operation`, since a
|
|
463
|
+
// `Server` must not carry a type parameter, and a runner can only run one
|
|
464
|
+
// at its own operation set.
|
|
465
|
+
/** @type {_VirtualServer} */
|
|
466
|
+
const server = { listener: /** @type {_VirtualListener} */ (listener) }
|
|
467
|
+
/** @type {Server} */
|
|
468
|
+
const handle = asNominalServer(server)
|
|
469
|
+
return [state, ok(handle)]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Takes the address, then hands `server`'s listener every queued request in
|
|
474
|
+
* turn, threading the state through each and recording what came back. The
|
|
475
|
+
* queue is emptied, so a second `listen` does not re-deliver.
|
|
476
|
+
*
|
|
477
|
+
* **Binding can fail here, because it can fail on a host.** A port outside
|
|
478
|
+
* `0`–`65535` or not an integer is `ERR_SOCKET_BAD_PORT`, an address already
|
|
479
|
+
* taken is `EADDRINUSE`, and a server asked to listen twice is
|
|
480
|
+
* `ERR_SERVER_ALREADY_LISTEN` — the three failures Node reports, in the shape it
|
|
481
|
+
* reports them. Port `0` is the exception to the second: it names no port, so
|
|
482
|
+
* two servers asking for one do not collide. `Listen` became fallible precisely to carry them, and a runner
|
|
483
|
+
* that always succeeded would make a program that mishandles either look
|
|
484
|
+
* correct.
|
|
485
|
+
*
|
|
486
|
+
* **`unwrap` is total, and the type system is what makes it so.** A
|
|
487
|
+
* `RequestListener`'s channel is `never`, and a listener reaches that by
|
|
488
|
+
* absorbing every `Result` its effects produce — through `resultStep` /
|
|
489
|
+
* `resultMapStep`, which receive the whole `Result`, including this runner's
|
|
490
|
+
* `notImplemented` for an operation it lacks. So a listener that calls `exec`
|
|
491
|
+
* here does not fail: it is handed the refusal and answers with a response
|
|
492
|
+
* frame, which is the contract `RequestListener` states.
|
|
493
|
+
*
|
|
494
|
+
* A listener that propagated instead would make this throw — but such a
|
|
495
|
+
* listener does not type-check in that position (`Type 'IoChannel' is not
|
|
496
|
+
* assignable to type 'never'`), so the throw is the checked assertion of an
|
|
497
|
+
* invariant, not a case to handle. Handling it defensively would add a branch
|
|
498
|
+
* nothing can reach, which the coverage gate rejects and
|
|
499
|
+
* [`fjs/AGENTS.md`](../../../AGENTS.md) §1.2 tells you to restructure away.
|
|
500
|
+
*
|
|
501
|
+
* The listener comes out of the handle rather than out of the state, so which
|
|
502
|
+
* server was asked to listen is the one that answers — see {@link createServer}.
|
|
503
|
+
*
|
|
504
|
+
* @type {(server: Server, port: number, host: string) => (state: State) => readonly[State, IoResult<void>]}
|
|
505
|
+
*/
|
|
506
|
+
const listen = (server, port, host) => state => {
|
|
507
|
+
// Asked **first**, as the Node runner asks it: an empty host is refused
|
|
508
|
+
// before anything else is looked at, including whether this server is
|
|
509
|
+
// already listening. Node forms no opinion about `''` at all — it binds —
|
|
510
|
+
// so there is no host order to copy here and the two runners simply have to
|
|
511
|
+
// pick the same one. They did not at first: this check sat below the
|
|
512
|
+
// listening one, and an already-listening server retried with `''` reported
|
|
513
|
+
// `ERR_SERVER_ALREADY_LISTEN` here against `ERR_INVALID_ARG_VALUE` there.
|
|
514
|
+
if (host === emptyHost) { return [state, error(emptyHostError)] }
|
|
515
|
+
const bound = /** @type {_VirtualServer} */ (asBaseServer(server))
|
|
516
|
+
const { listener } = bound
|
|
517
|
+
// Asked **before** the port, because that is the order Node asks in: a
|
|
518
|
+
// server already listening reports `ERR_SERVER_ALREADY_LISTEN` for `-1`,
|
|
519
|
+
// `1.5`, `65536` and `NaN` alike — checked on Linux with Node 22.22.2 and on
|
|
520
|
+
// Darwin with Node 23.11.0, where the same values on a fresh server all
|
|
521
|
+
// report `ERR_SOCKET_BAD_PORT`. Which of two true things a failure names is
|
|
522
|
+
// part of what a runner promises, so a program branching on the code
|
|
523
|
+
// branches the same way here as there.
|
|
524
|
+
if (state.listening.some(b => b.server === bound)) {
|
|
525
|
+
return [state, error(ioError({
|
|
526
|
+
code: 'ERR_SERVER_ALREADY_LISTEN',
|
|
527
|
+
message: 'Listen method has been called more than once without closing.',
|
|
528
|
+
}))]
|
|
529
|
+
}
|
|
530
|
+
if (!isPort(port)) {
|
|
531
|
+
return [state, error(ioError({
|
|
532
|
+
code: 'ERR_SOCKET_BAD_PORT',
|
|
533
|
+
// Byte-for-byte what Node says, type included: this runner claims
|
|
534
|
+
// to report failures in the shape the host reports them, and a
|
|
535
|
+
// message that is nearly right is a claim that is not.
|
|
536
|
+
message: `options.port should be >= 0 and < 65536. Received type number (${port}).`,
|
|
537
|
+
}))]
|
|
538
|
+
}
|
|
539
|
+
// Lower-cased because a DNS name is case-insensitive and so is the
|
|
540
|
+
// hexadecimal of an IPv6 literal: `LOCALHOST` and `localhost` are one
|
|
541
|
+
// address, and a host refuses the second bind — checked on Linux with Node
|
|
542
|
+
// 22.22.2 and on Darwin with Node 23.11.0. Unlike the wildcard rules in
|
|
543
|
+
// [address-model](./todo/address-model.md), that equivalence is the same
|
|
544
|
+
// everywhere, so it is safe to model.
|
|
545
|
+
const address = `${host.toLowerCase()}:${port}`
|
|
546
|
+
// Port `0` asks the host for whichever port is free, so two servers that
|
|
547
|
+
// ask both get one — checked on Linux with Node 22.22.2 and on Darwin with
|
|
548
|
+
// Node 23.11.0: they come back bound to different ports.
|
|
549
|
+
// Comparing `host:0` to `host:0` would refuse the second, which is the worse
|
|
550
|
+
// direction to be wrong in: a runner that rejects a program a host accepts
|
|
551
|
+
// stops work that would have run.
|
|
552
|
+
if (port !== ephemeral && state.listening.some(b => b.address === address)) {
|
|
553
|
+
return [state, error(ioError({
|
|
554
|
+
code: 'EADDRINUSE',
|
|
555
|
+
message: `listen EADDRINUSE: address already in use ${address}`,
|
|
556
|
+
}))]
|
|
557
|
+
}
|
|
558
|
+
/** @type {State} */
|
|
559
|
+
let s = { ...state, listening: [...state.listening, { address, server: bound }], requests: [] }
|
|
560
|
+
for (const request of state.requests) {
|
|
561
|
+
const [next, response] = virtual(s)(listener(request))
|
|
562
|
+
s = { ...next, responses: [...next.responses, unwrap(response)] }
|
|
563
|
+
}
|
|
564
|
+
return [s, okVoid]
|
|
565
|
+
}
|
|
566
|
+
|
|
363
567
|
/** @type {PartialMemOperationMap<NodeOp, State>} */
|
|
364
568
|
const map = {
|
|
365
569
|
all: (...a) => state => {
|
|
@@ -409,6 +613,8 @@ const map = {
|
|
|
409
613
|
createExclusive,
|
|
410
614
|
writeBytes: writeBytesOp,
|
|
411
615
|
stat: statOp,
|
|
616
|
+
createServer,
|
|
617
|
+
listen,
|
|
412
618
|
randomInt: () => state => [{ ...state, randomNext: state.randomNext + 1 }, ok(state.randomNext)],
|
|
413
619
|
now: () => state => [state, ok(state.epochNs)],
|
|
414
620
|
// Virtual sandbox is a pass-through: the fixture's test function is
|
|
@@ -435,14 +641,22 @@ const map = {
|
|
|
435
641
|
/**
|
|
436
642
|
* The virtual runner.
|
|
437
643
|
*
|
|
438
|
-
* **It implements part of `NodeOp`, and says so.** `exec`, `
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
644
|
+
* **It implements part of `NodeOp`, and says so.** `exec`, `forever` and `test`
|
|
645
|
+
* have no meaning against an in-memory filesystem, and they used to be present
|
|
646
|
+
* as `todo` handlers — entries that existed only to satisfy a total operation
|
|
647
|
+
* map and threw when reached. They are simply absent now, so a program that
|
|
648
|
+
* asks for one gets `error(notImplemented)` back through its own continuation
|
|
649
|
+
* and decides what an incompatible runner means for it, which is what
|
|
650
|
+
* `NotImplemented` was introduced for. A command that is not a `NodeOp` at all
|
|
651
|
+
* still panics.
|
|
652
|
+
*
|
|
653
|
+
* `forever` is absent for a reason no implementation could remove: its result
|
|
654
|
+
* type is `Result<never, NotImplemented>`, so `error(notImplemented)` is the
|
|
655
|
+
* *only* value it can produce — a runner that cannot block forever has nothing
|
|
656
|
+
* else to answer, and a server program run here therefore ends where it would
|
|
657
|
+
* otherwise have blocked. `createServer` and `listen` do have meanings without
|
|
658
|
+
* a socket and are implemented above, which is what makes the rest of such a
|
|
659
|
+
* program provable.
|
|
446
660
|
*
|
|
447
661
|
* @type {RunInstance<NodeOp, State>}
|
|
448
662
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Dir } from './types.ts'
|
|
3
|
-
* @import { NodeOp } from '../types.ts'
|
|
2
|
+
* @import { Dir, State } from './types.ts'
|
|
3
|
+
* @import { IncomingMessage, NodeOp, RequestListener } from '../types.ts'
|
|
4
4
|
* @import { Effect } from '../../types.ts'
|
|
5
5
|
* @import { IoChannel } from '../types.ts'
|
|
6
6
|
*/
|
|
@@ -51,6 +51,7 @@ export declare const proof: {
|
|
|
51
51
|
renameNonEmptyDirOverEmptyDir: () => void;
|
|
52
52
|
renameEmptyDirOverNonEmptyDir: () => void;
|
|
53
53
|
renameFileOntoDirectory: () => void;
|
|
54
|
+
renameForeignArrayFileOntoDirectory: () => void;
|
|
54
55
|
readFileTooLarge: () => void;
|
|
55
56
|
readBytesNegativeSize: () => void;
|
|
56
57
|
readBytesZeroSize: () => void;
|
|
@@ -59,6 +60,20 @@ export declare const proof: {
|
|
|
59
60
|
readBytesFractionalOffset: () => void;
|
|
60
61
|
readBytesAcrossChunkBoundary: () => void;
|
|
61
62
|
writeBytesWrongOffset: () => void;
|
|
63
|
+
statOnDirectory: () => void;
|
|
64
|
+
statOnEmptyPath: () => void;
|
|
62
65
|
statOnJsModule: () => void;
|
|
63
66
|
largeFileReadBytes: () => void;
|
|
67
|
+
http: {
|
|
68
|
+
answersQueuedRequests: () => void;
|
|
69
|
+
dispatchesThroughTheHandle: () => void;
|
|
70
|
+
badPort: () => void;
|
|
71
|
+
ephemeralPorts: () => void;
|
|
72
|
+
addressInUse: () => void;
|
|
73
|
+
addressInUseIgnoresCase: () => void;
|
|
74
|
+
emptyHostRefused: () => void;
|
|
75
|
+
emptyHostBeatsAlreadyListening: () => void;
|
|
76
|
+
alreadyListening: () => void;
|
|
77
|
+
foreverIsNotImplemented: () => void;
|
|
78
|
+
};
|
|
64
79
|
};
|