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.
Files changed (178) hide show
  1. package/fjs/basen/cbase32/module.f.mjs +18 -10
  2. package/fjs/basen/cbase32/proof.f.d.mts +3 -0
  3. package/fjs/basen/cbase32/proof.f.mjs +24 -2
  4. package/fjs/basen/proof.f.mjs +8 -16
  5. package/fjs/bnf/ll1/proof.f.d.mts +2 -0
  6. package/fjs/bnf/ll1/proof.f.mjs +33 -18
  7. package/fjs/bnf/module.f.d.mts +2 -2
  8. package/fjs/bnf/module.f.mjs +2 -2
  9. package/fjs/cas/proof.f.mjs +4 -26
  10. package/fjs/ci/common/module.f.mjs +6 -0
  11. package/fjs/ci/config/module.f.d.mts +7 -7
  12. package/fjs/ci/config/module.f.mjs +7 -7
  13. package/fjs/cli/module.f.mjs +14 -10
  14. package/fjs/crypto/vdf/module.f.d.mts +0 -1
  15. package/fjs/crypto/vdf/module.f.mjs +3 -14
  16. package/fjs/dev/module.f.mjs +10 -20
  17. package/fjs/djs/parser/module.f.d.mts +101 -53
  18. package/fjs/djs/parser/module.f.mjs +732 -525
  19. package/fjs/djs/parser/proof.f.d.mts +6 -0
  20. package/fjs/djs/parser/proof.f.mjs +315 -11
  21. package/fjs/djs/parser/types.d.ts +29 -2
  22. package/fjs/djs/tokenizer/module.f.mjs +122 -105
  23. package/fjs/djs/tokenizer/proof.f.mjs +17 -0
  24. package/fjs/djs/transpiler/module.f.mjs +4 -5
  25. package/fjs/djs/transpiler/proof.f.d.mts +2 -0
  26. package/fjs/djs/transpiler/proof.f.mjs +25 -0
  27. package/fjs/edag/amnesia/module.f.d.mts +20 -0
  28. package/fjs/edag/amnesia/module.f.mjs +303 -0
  29. package/fjs/edag/amnesia/proof.f.d.mts +79 -0
  30. package/fjs/edag/amnesia/proof.f.mjs +573 -0
  31. package/fjs/edag/amnesia/types.d.ts +15 -0
  32. package/fjs/edag/module.f.d.mts +410 -0
  33. package/fjs/edag/module.f.mjs +559 -0
  34. package/fjs/edag/proof.f.d.mts +116 -0
  35. package/fjs/edag/proof.f.mjs +686 -0
  36. package/fjs/edag/types.d.ts +55 -0
  37. package/fjs/effects/node/memory/module.d.mts +37 -3
  38. package/fjs/effects/node/memory/module.mjs +38 -4
  39. package/fjs/effects/node/memory/proof.d.mts +5 -2
  40. package/fjs/effects/node/memory/proof.mjs +36 -6
  41. package/fjs/effects/node/module.d.mts +9 -1
  42. package/fjs/effects/node/module.f.d.mts +39 -0
  43. package/fjs/effects/node/module.f.mjs +46 -0
  44. package/fjs/effects/node/module.mjs +234 -32
  45. package/fjs/effects/node/types.d.ts +28 -2
  46. package/fjs/effects/node/virtual/module.f.d.mts +19 -10
  47. package/fjs/effects/node/virtual/module.f.mjs +246 -32
  48. package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
  49. package/fjs/effects/node/virtual/proof.f.mjs +232 -8
  50. package/fjs/effects/node/virtual/types.d.ts +57 -1
  51. package/fjs/emergent_testing/browser/proof.d.mts +55 -0
  52. package/fjs/emergent_testing/browser/proof.mjs +352 -0
  53. package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
  54. package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
  55. package/fjs/emergent_testing/browser.d.mts +79 -0
  56. package/fjs/emergent_testing/browser.mjs +432 -0
  57. package/fjs/emergent_testing/proof.f.d.mts +1 -1
  58. package/fjs/emergent_testing/proof.f.mjs +2 -2
  59. package/fjs/js/string_escape/module.f.d.mts +47 -0
  60. package/fjs/js/string_escape/module.f.mjs +95 -0
  61. package/fjs/js/string_escape/proof.f.d.mts +8 -0
  62. package/fjs/js/string_escape/proof.f.mjs +68 -0
  63. package/fjs/js/tokenizer/module.f.mjs +14 -12
  64. package/fjs/mcp/cas/module.f.d.mts +1 -7
  65. package/fjs/mcp/cas/module.f.mjs +24 -15
  66. package/fjs/mcp/evo/module.f.mjs +9 -24
  67. package/fjs/mcp/module.f.d.mts +3 -1
  68. package/fjs/mcp/module.f.mjs +4 -2
  69. package/fjs/media/json/rtti/module.f.d.mts +2 -2
  70. package/fjs/media/json/rtti/module.f.mjs +1 -1
  71. package/fjs/media/json/schema/module.f.d.mts +15 -5
  72. package/fjs/media/json/schema/module.f.mjs +42 -15
  73. package/fjs/media/json/schema/proof.f.d.mts +10 -1
  74. package/fjs/media/json/schema/proof.f.mjs +88 -16
  75. package/fjs/media/json/serializer/module.f.mjs +14 -26
  76. package/fjs/media/json/types.d.ts +1 -1
  77. package/fjs/media/lock/module.f.d.mts +8 -3
  78. package/fjs/media/lock/module.f.mjs +9 -3
  79. package/fjs/media/lock/proof.f.d.mts +1 -1
  80. package/fjs/media/lock/proof.f.mjs +2 -2
  81. package/fjs/media/lock/types.d.ts +1 -3
  82. package/fjs/media/module.f.d.mts +20 -13
  83. package/fjs/media/module.f.mjs +29 -16
  84. package/fjs/media/note/module.f.d.mts +14 -7
  85. package/fjs/media/note/module.f.mjs +14 -7
  86. package/fjs/media/note/proof.f.mjs +1 -1
  87. package/fjs/media/proof.f.mjs +5 -5
  88. package/fjs/media/revision/module.f.d.mts +8 -2
  89. package/fjs/media/revision/module.f.mjs +9 -3
  90. package/fjs/media/revision/proof.f.mjs +1 -1
  91. package/fjs/media/revision/types.d.ts +1 -1
  92. package/fjs/media/type/module.f.d.mts +20 -0
  93. package/fjs/media/type/module.f.mjs +74 -0
  94. package/fjs/media/type/proof.f.d.mts +5 -0
  95. package/fjs/media/type/proof.f.mjs +24 -1
  96. package/fjs/module.f.mjs +6 -0
  97. package/fjs/path/module.f.d.mts +60 -3
  98. package/fjs/path/module.f.mjs +138 -14
  99. package/fjs/path/proof.f.d.mts +3 -0
  100. package/fjs/path/proof.f.mjs +298 -2
  101. package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
  102. package/fjs/protocol/json_rpc/module.f.mjs +49 -13
  103. package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
  104. package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
  105. package/fjs/protocol/json_rpc/types.d.ts +12 -2
  106. package/fjs/protocol/mcp/module.f.d.mts +89 -54
  107. package/fjs/protocol/mcp/module.f.mjs +110 -56
  108. package/fjs/protocol/mcp/proof.f.d.mts +13 -0
  109. package/fjs/protocol/mcp/proof.f.mjs +107 -7
  110. package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
  111. package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
  112. package/fjs/protocol/mcp/types.d.ts +14 -2
  113. package/fjs/text/utf8/module.f.mjs +27 -1
  114. package/fjs/text/utf8/proof.f.mjs +37 -0
  115. package/fjs/types/array/module.f.mjs +18 -11
  116. package/fjs/types/array/proof.f.mjs +27 -1
  117. package/fjs/types/bigfloat/module.f.d.mts +48 -9
  118. package/fjs/types/bigfloat/module.f.mjs +147 -59
  119. package/fjs/types/bigfloat/proof.f.d.mts +2 -0
  120. package/fjs/types/bigfloat/proof.f.mjs +136 -4
  121. package/fjs/types/bigfloat/types.d.ts +17 -0
  122. package/fjs/types/function/module.f.d.mts +11 -2
  123. package/fjs/types/function/module.f.mjs +20 -2
  124. package/fjs/types/function/proof.f.mjs +5 -1
  125. package/fjs/types/phantom/types.d.ts +21 -0
  126. package/fjs/types/result/module.f.d.mts +4 -4
  127. package/fjs/types/result/module.f.mjs +2 -2
  128. package/fjs/types/result/proof.f.d.mts +6 -2
  129. package/fjs/types/result/proof.f.mjs +17 -1
  130. package/fjs/types/rtti/common/module.f.d.mts +78 -5
  131. package/fjs/types/rtti/common/module.f.mjs +168 -3
  132. package/fjs/types/rtti/common/proof.f.d.mts +14 -0
  133. package/fjs/types/rtti/common/proof.f.mjs +61 -2
  134. package/fjs/types/rtti/common/types.d.ts +23 -1
  135. package/fjs/types/rtti/data/module.f.d.mts +57 -1
  136. package/fjs/types/rtti/data/module.f.mjs +241 -47
  137. package/fjs/types/rtti/data/proof.f.d.mts +10 -0
  138. package/fjs/types/rtti/data/proof.f.mjs +272 -39
  139. package/fjs/types/rtti/data/types.d.ts +14 -7
  140. package/fjs/types/rtti/host.proof.d.mts +33 -0
  141. package/fjs/types/rtti/host.proof.mjs +170 -0
  142. package/fjs/types/rtti/module.f.d.mts +51 -6
  143. package/fjs/types/rtti/module.f.mjs +50 -3
  144. package/fjs/types/rtti/parse/module.f.d.mts +31 -24
  145. package/fjs/types/rtti/parse/module.f.mjs +197 -59
  146. package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
  147. package/fjs/types/rtti/parse/proof.f.mjs +171 -47
  148. package/fjs/types/rtti/proof.f.d.mts +6 -0
  149. package/fjs/types/rtti/proof.f.mjs +42 -1
  150. package/fjs/types/rtti/ts/module.f.d.mts +9 -3
  151. package/fjs/types/rtti/ts/module.f.mjs +68 -15
  152. package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
  153. package/fjs/types/rtti/ts/proof.f.mjs +139 -6
  154. package/fjs/types/rtti/ts/types.d.ts +164 -16
  155. package/fjs/types/rtti/types.d.ts +31 -3
  156. package/fjs/types/rtti/validate/module.f.d.mts +42 -25
  157. package/fjs/types/rtti/validate/module.f.mjs +187 -53
  158. package/fjs/types/rtti/validate/proof.f.d.mts +28 -2
  159. package/fjs/types/rtti/validate/proof.f.mjs +430 -75
  160. package/fjs/types/ts/types.d.ts +4 -0
  161. package/fjs/web/module.f.d.mts +115 -0
  162. package/fjs/web/module.f.mjs +581 -0
  163. package/fjs/web/proof.f.d.mts +39 -0
  164. package/fjs/web/proof.f.mjs +413 -0
  165. package/fjs/web/types.d.ts +39 -0
  166. package/fjs/website/browser-prepare.d.mts +5 -0
  167. package/fjs/website/browser-prepare.mjs +115 -0
  168. package/fjs/website/browser-source.d.mts +37 -0
  169. package/fjs/website/browser-source.mjs +202 -0
  170. package/fjs/website/browser-source.proof.d.mts +31 -0
  171. package/fjs/website/browser-source.proof.mjs +162 -0
  172. package/fjs/website/browser.d.mts +5 -0
  173. package/fjs/website/browser.mjs +8 -0
  174. package/fjs/website/module.f.d.mts +3 -3
  175. package/fjs/website/module.f.mjs +62 -9
  176. package/fjs/website/proof.f.d.mts +4 -0
  177. package/fjs/website/proof.f.mjs +18 -3
  178. package/package.json +2 -3
@@ -1,20 +1,31 @@
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
  */
7
7
 
8
8
  import { assert, assertEq } from '../../../asserts/module.f.mjs'
9
- import { access, awaitIfPromise, exec, fetch, log, rm, writeFile, readFile, readdir, import_, rename, readBytes, writeBytes, stat, createExclusive } from '../module.f.mjs'
9
+ import { access, awaitIfPromise, exec, fetch, log, rm, writeFile, readFile, readdir, import_, rename, readBytes, writeBytes, stat, createExclusive, createServer, forever, listen } from '../module.f.mjs'
10
10
  import { empty, length, maxLengthBytes, vec, vec8 } from '../../../types/bit_vec/module.f.mjs'
11
+ import { history, historyStep, pureOk, step } from '../../module.f.mjs'
12
+ import { utf8, utf8ToString } from '../../../text/module.f.mjs'
11
13
  import { emptyState, virtual } from './module.f.mjs'
12
14
  import { do_ } from '../../module.f.mjs'
13
15
  import { catchStep } from '../../module.f.mjs'
14
16
 
15
17
  /**
16
- * Asserts that a channel error is a host failure carrying `message` — the
18
+ * Asserts that a channel error is a host failure carrying `code` — the
17
19
  * normalized shape every runner reports, virtual and Node alike.
20
+ * @type {(e: IoChannel, code: string) => void}
21
+ */
22
+ const assertIoCode = (e, code) => {
23
+ assert(e[0] === 'ioError', e)
24
+ assertEq(e[1].code, code)
25
+ }
26
+
27
+ /**
28
+ * Asserts that a channel error is a host failure carrying `message`.
18
29
  * @type {(e: IoChannel, message: string) => void}
19
30
  */
20
31
  const assertIoMessage = (e, message) => {
@@ -334,6 +345,18 @@ export const proof = {
334
345
  const [, result] = virtual({ ...emptyState, root })(rename('myfile', 'mydir'))
335
346
  assert(result[0] === 'error')
336
347
  },
348
+ renameForeignArrayFileOntoDirectory: () => {
349
+ // An array with another constructor's prototype models the observable
350
+ // instanceof behavior of an array received from another realm.
351
+ const foreignFile = Reflect.construct(Array, [], Object)
352
+ assert(Array.isArray(foreignFile))
353
+ assert(!(foreignFile instanceof Array))
354
+ /** @type {Dir} */
355
+ const root = { 'myfile': foreignFile, 'mydir': {} }
356
+ const [, result] = virtual({ ...emptyState, root })(rename('myfile', 'mydir'))
357
+ assert(result[0] === 'error')
358
+ assertIoMessage(result[1], "'mydir' is a directory")
359
+ },
337
360
  readFileTooLarge: () => {
338
361
  // a file stored as two max-size chunks exceeds the limit; readFile must return an error
339
362
  const chunk0 = vec(maxLengthBytes * 8n)(0n)
@@ -396,14 +419,38 @@ export const proof = {
396
419
  const [, result] = virtual({ ...emptyState, root })(writeBytes('file', 5, vec8(0x43n)))
397
420
  assert(result[0] === 'error')
398
421
  },
422
+ statOnDirectory: () => {
423
+ // A host stats a directory successfully and says it is not a file. The
424
+ // empty remaining path is how one arrives here: `operation` has already
425
+ // descended into it.
426
+ /** @type {Dir} */
427
+ const root = { docs: { 'index.html': [vec8(0x41n)] } }
428
+ const [, result] = virtual({ ...emptyState, root })(stat('docs'))
429
+ assert(result[0] === 'ok', result)
430
+ assertEq(result[1].isFile, false)
431
+ },
432
+ statOnEmptyPath: () => {
433
+ // An empty path is not the root, though `parse` collapses both to no
434
+ // segments at all. A host answers `ENOENT`, so this does.
435
+ const [, result] = virtual(emptyState)(stat(''))
436
+ assert(result[0] === 'error', result)
437
+ assertIoCode(result[1], 'ENOENT')
438
+ // `.` *is* the root, and stats as the directory it is.
439
+ const [, root] = virtual(emptyState)(stat('.'))
440
+ assert(root[0] === 'ok', root)
441
+ assertEq(root[1].isFile, false)
442
+ },
399
443
  statOnJsModule: () => {
400
- // stat on a JsModule entry (neither an array nor a descendable
401
- // directory) covers the !Array.isArray(file) branch of statOp.
444
+ // A `JsModule` entry is this file system's non-regular name: it exists
445
+ // and stats fine, and says it is not a file the shape a host reports
446
+ // for a FIFO or a device, and what a caller's guard against reading one
447
+ // has to be able to see.
402
448
  /** @type {Dir} */
403
449
  const root = { 'a.f.ts': () => ({}) }
404
450
  const [, result] = virtual({ ...emptyState, root })(stat('a.f.ts'))
405
- assert(result[0] === 'error')
406
- assertIoMessage(result[1], `'a.f.ts' is not a file`)
451
+ assert(result[0] === 'ok', result)
452
+ assertEq(result[1].isFile, false)
453
+ assertEq(result[1].size, 0)
407
454
  },
408
455
  largeFileReadBytes: () => {
409
456
  // A file stored as two 128 KiB chunks is larger than maxLengthBytes.
@@ -416,4 +463,181 @@ export const proof = {
416
463
  const [, result] = virtual({ ...emptyState, root })(readBytes('large', chunkSize, 1))
417
464
  assert(result[0] === 'ok')
418
465
  },
466
+ // A server without a socket: `createServer` stores the listener and
467
+ // `listen` hands it the requests the fixture queued, which is what makes a
468
+ // request-in / response-out proof possible here at all.
469
+ http: {
470
+ answersQueuedRequests: () => {
471
+ /** @type {(url: string) => IncomingMessage} */
472
+ const get = url => ({ method: 'GET', url, headers: {}, body: empty })
473
+ /** @type {RequestListener<never>} */
474
+ const listener = ({ url }) =>
475
+ pureOk({ status: 200, headers: {}, body: utf8(`echo ${url}`) })
476
+ const e = step(createServer(listener), server => listen(server, 8080, '127.0.0.1'))
477
+ /** @type {State} */
478
+ const state = { ...emptyState, requests: [get('/a'), get('/b')] }
479
+ const [s, result] = virtual(state)(e)
480
+ assert(result[0] === 'ok', result)
481
+ assertEq(s.listening.map(b => b.address).join(), '127.0.0.1:8080')
482
+ // The queue is emptied, so a second `listen` cannot answer the same
483
+ // request twice.
484
+ assertEq(s.requests.length, 0)
485
+ assertEq(s.responses.map(r => utf8ToString(r.body)).join(', '), 'echo /a, echo /b')
486
+ },
487
+ // Two servers in one program are two servers here, as they are on a
488
+ // host: `listen` answers with the listener its *handle* carries, not
489
+ // with whichever was created last.
490
+ dispatchesThroughTheHandle: () => {
491
+ /** @type {(name: string) => RequestListener<never>} */
492
+ const named = name => () => pureOk({ status: 200, headers: {}, body: utf8(name) })
493
+ // Flat, because the third link needs the *first* one's value: a
494
+ // history carries `a` forward instead of a nested continuation
495
+ // closing over it.
496
+ const created = history(createServer(named('a')))
497
+ const both = historyStep(created, () => createServer(named('b')))
498
+ const first = step(both, ([, a]) => listen(a, 8080, '127.0.0.1'))
499
+ /** @type {State} */
500
+ const state = {
501
+ ...emptyState,
502
+ requests: [{ method: 'GET', url: '/', headers: {}, body: empty }],
503
+ }
504
+ const [s, result] = virtual(state)(first)
505
+ assert(result[0] === 'ok', result)
506
+ assertEq(utf8ToString(s.responses[0].body), 'a')
507
+ },
508
+ // A port a host would refuse is refused here, or a program that cannot
509
+ // run anywhere could still be proven.
510
+ badPort: () => {
511
+ /** @type {RequestListener<never>} */
512
+ const listener = () => pureOk({ status: 200, headers: {}, body: empty })
513
+ /** @type {(port: number) => void} */
514
+ const rejects = port => {
515
+ const e = step(createServer(listener), server => listen(server, port, '127.0.0.1'))
516
+ const [s, result] = virtual(emptyState)(e)
517
+ assert(result[0] === 'error', result)
518
+ assertIoCode(result[1], 'ERR_SOCKET_BAD_PORT')
519
+ // The message is Node's own, type and all — the shape is the
520
+ // claim this runner makes.
521
+ assertIoMessage(
522
+ result[1],
523
+ `options.port should be >= 0 and < 65536. Received type number (${port}).`)
524
+ assertEq(s.listening.length, 0)
525
+ }
526
+ rejects(-1)
527
+ rejects(1.5)
528
+ rejects(65536)
529
+ rejects(NaN)
530
+ },
531
+ // Port `0` names no port: two servers asking the host for a free one
532
+ // both get one, so refusing the second would reject a program that runs.
533
+ ephemeralPorts: () => {
534
+ /** @type {RequestListener<never>} */
535
+ const listener = () => pureOk({ status: 200, headers: {}, body: empty })
536
+ const first = history(createServer(listener))
537
+ const second = historyStep(first, () => createServer(listener))
538
+ const bound = historyStep(second, b => listen(b, 0, '127.0.0.1'))
539
+ const e = step(bound, ([, , a]) => listen(a, 0, '127.0.0.1'))
540
+ const [s, result] = virtual(emptyState)(e)
541
+ assert(result[0] === 'ok', result)
542
+ assertEq(s.listening.length, 2)
543
+ },
544
+ // Binding fails here the way it fails on a host, which is the whole
545
+ // point of `Listen` being fallible: a program that mishandles either
546
+ // failure must not look correct against this runner.
547
+ addressInUse: () => {
548
+ /** @type {RequestListener<never>} */
549
+ const listener = () => pureOk({ status: 200, headers: {}, body: empty })
550
+ const first = history(createServer(listener))
551
+ const second = historyStep(first, () => createServer(listener))
552
+ // `historyStep` spreads the history over its continuation, newest
553
+ // first: `b` here is the second server, and `a` the first.
554
+ const bound = historyStep(second, b => listen(b, 8080, '127.0.0.1'))
555
+ const e = step(bound, ([, , a]) => listen(a, 8080, '127.0.0.1'))
556
+ const [s, result] = virtual(emptyState)(e)
557
+ assert(result[0] === 'error', result)
558
+ assertIoCode(result[1], 'EADDRINUSE')
559
+ // The first server keeps the address it took.
560
+ assertEq(s.listening.length, 1)
561
+ },
562
+ // A DNS name is case-insensitive, so `LOCALHOST` takes the address
563
+ // `localhost` then asks for — checked on Linux with Node 22.22.2 and on
564
+ // Darwin with Node 23.11.0, where the second bind is `EADDRINUSE`.
565
+ addressInUseIgnoresCase: () => {
566
+ /** @type {RequestListener<never>} */
567
+ const listener = () => pureOk({ status: 200, headers: {}, body: empty })
568
+ const first = history(createServer(listener))
569
+ const second = historyStep(first, () => createServer(listener))
570
+ const bound = historyStep(second, b => listen(b, 8080, 'LOCALHOST'))
571
+ const e = step(bound, ([, , a]) => listen(a, 8080, 'localhost'))
572
+ const [s, result] = virtual(emptyState)(e)
573
+ assert(result[0] === 'error', result)
574
+ assertIoCode(result[1], 'EADDRINUSE')
575
+ assertIoMessage(
576
+ result[1],
577
+ 'listen EADDRINUSE: address already in use localhost:8080')
578
+ // Recorded lower-cased, whichever case asked for it.
579
+ assertEq(s.listening.length, 1)
580
+ assertEq(s.listening[0].address, 'localhost:8080')
581
+ },
582
+ // `''` is the host a program did not state, and Node binds every
583
+ // interface for it — so both runners refuse it rather than forward it.
584
+ emptyHostRefused: () => {
585
+ /** @type {RequestListener<never>} */
586
+ const listener = () => pureOk({ status: 200, headers: {}, body: empty })
587
+ const created = history(createServer(listener))
588
+ const e = step(created, ([server]) => listen(server, 8080, ''))
589
+ const [s, result] = virtual(emptyState)(e)
590
+ assert(result[0] === 'error', result)
591
+ assertIoCode(result[1], 'ERR_INVALID_ARG_VALUE')
592
+ assertIoMessage(
593
+ result[1],
594
+ `The argument 'host' must not be empty. Received ''`)
595
+ // Nothing bound.
596
+ assertEq(s.listening.length, 0)
597
+ },
598
+ // And it is refused *first*: a server already listening, retried with
599
+ // an empty host, names the host and not the state. Node has no order of
600
+ // its own to copy here — it binds `''` — so the two runners have only
601
+ // to agree, and the Node runner asks this before it touches the socket.
602
+ emptyHostBeatsAlreadyListening: () => {
603
+ /** @type {RequestListener<never>} */
604
+ const listener = () => pureOk({ status: 200, headers: {}, body: empty })
605
+ const created = history(createServer(listener))
606
+ const bound = historyStep(created, server => listen(server, 8080, '127.0.0.1'))
607
+ const e = step(bound, ([, server]) => listen(server, 9090, ''))
608
+ const [, result] = virtual(emptyState)(e)
609
+ assert(result[0] === 'error', result)
610
+ assertIoCode(result[1], 'ERR_INVALID_ARG_VALUE')
611
+ },
612
+ alreadyListening: () => {
613
+ /** @type {RequestListener<never>} */
614
+ const listener = () => pureOk({ status: 200, headers: {}, body: empty })
615
+ /** @type {(second: number) => IoChannel} */
616
+ const again = second => {
617
+ const created = history(createServer(listener))
618
+ const bound = historyStep(created, server => listen(server, 8080, '127.0.0.1'))
619
+ const e = step(bound, ([, server]) => listen(server, second, '127.0.0.1'))
620
+ const [, result] = virtual(emptyState)(e)
621
+ assert(result[0] === 'error', result)
622
+ return result[1]
623
+ }
624
+ assertIoCode(again(9090), 'ERR_SERVER_ALREADY_LISTEN')
625
+ // And it is asked before the port is: a server already listening
626
+ // reports this for a port no server could take, where the same
627
+ // value on a fresh server is `ERR_SOCKET_BAD_PORT`. That is the
628
+ // order Node asks in, checked on Linux with Node 22.22.2 and on
629
+ // Darwin with Node 23.11.0.
630
+ assertIoCode(again(-1), 'ERR_SERVER_ALREADY_LISTEN')
631
+ assertIoCode(again(65536), 'ERR_SERVER_ALREADY_LISTEN')
632
+ assertIoCode(again(NaN), 'ERR_SERVER_ALREADY_LISTEN')
633
+ },
634
+ // `forever` is the operation this runner cannot answer — its result
635
+ // type leaves it nothing but `notImplemented` to return — so a server
636
+ // program run here ends where it would otherwise have blocked.
637
+ foreverIsNotImplemented: () => {
638
+ const [, result] = virtual(emptyState)(forever())
639
+ assert(result[0] === 'error', result)
640
+ assertEq(result[1][1], 'forever')
641
+ },
642
+ },
419
643
  }
@@ -5,7 +5,8 @@
5
5
  * @module
6
6
  */
7
7
  import type { Vec } from '../../../types/bit_vec/types.ts';
8
- import type { Module } from '../types.ts';
8
+ import type { Effect } from '../../types.ts';
9
+ import type { IncomingMessage, Module, NodeOp, ServerResponse } from '../types.ts';
9
10
  /**
10
11
  * In-memory JS module entry. When `import_` is called on the path, the
11
12
  * function is invoked and its return value is the module value (with a
@@ -20,6 +21,48 @@ export type _Entity = readonly Vec[] | Dir | JsModule;
20
21
  export type Dir = {
21
22
  readonly [name in string]?: _Entity;
22
23
  };
24
+ /**
25
+ * The listener a virtual `Server` handle carries, at the operation set this
26
+ * runner can actually run it with. `CreateServer` declares its listener over
27
+ * `Operation` — an unresolved type parameter would leak into every consumer of
28
+ * `Server` — so the handler narrows it, exactly as the Node runner does before
29
+ * handing a request to it. It rides in the handle rather than in the state, so
30
+ * that two servers in one program are two servers here too.
31
+ *
32
+ * @internal
33
+ */
34
+ export type _VirtualListener = (request: IncomingMessage) => Effect<NodeOp, ServerResponse, never>;
35
+ /**
36
+ * What a virtual `Server` handle carries.
37
+ *
38
+ * It is a record rather than the listener itself so that each `createServer`
39
+ * gets its own identity: on a host, creating two servers from one listener
40
+ * gives two servers, and a handle that *was* the listener would make them the
41
+ * same one — which `listen` would then read as the same server listening twice.
42
+ *
43
+ * @internal
44
+ */
45
+ export type _VirtualServer = {
46
+ readonly listener: _VirtualListener;
47
+ };
48
+ /**
49
+ * A server that is listening, and the address it took.
50
+ *
51
+ * The *server* is what is recorded, not its listener: two servers built from one
52
+ * listener are two servers, and only the second `listen` on the same **server**
53
+ * is the one Node refuses as already listening.
54
+ *
55
+ * @internal
56
+ */
57
+ export type _Binding = {
58
+ /**
59
+ * `host:port`, with the host lower-cased — a name is case-insensitive, and
60
+ * so is the hexadecimal of an IPv6 literal, so `LOCALHOST` and `localhost`
61
+ * are one address here as they are on a host.
62
+ */
63
+ readonly address: string;
64
+ readonly server: _VirtualServer;
65
+ };
23
66
  export type State = {
24
67
  stdout: string;
25
68
  stderr: string;
@@ -36,4 +79,17 @@ export type State = {
36
79
  };
37
80
  /** Monotonically increasing counter returned by `randomInt`; starts at 0. */
38
81
  randomNext: number;
82
+ /**
83
+ * What is listening, oldest first. An address appears once: a second
84
+ * `listen` on one that is taken fails, as it does on a host.
85
+ */
86
+ listening: readonly _Binding[];
87
+ /**
88
+ * The requests a fixture queues for the server to answer. `listen` delivers
89
+ * every one of them to the {@link _VirtualListener} its handle carries, and
90
+ * empties the queue — the virtual counterpart of accepting connections.
91
+ */
92
+ requests: readonly IncomingMessage[];
93
+ /** What the listener answered, oldest first. */
94
+ responses: readonly ServerResponse[];
39
95
  };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Proofs for the browser runner.
3
+ *
4
+ * The runner reaches the page only through the root element it is handed, so
5
+ * the DOM stand-in below is enough to drive every rendering branch from Node —
6
+ * no headless browser, and no global `window`/`document` for these proofs to
7
+ * install and unset.
8
+ */
9
+ export type _Element = {
10
+ readonly tag: string;
11
+ readonly attributes: Map<string, string>;
12
+ readonly ownerDocument: _Document;
13
+ textContent: string;
14
+ children: readonly _Element[];
15
+ readonly setAttribute: (name: string, value: string) => void;
16
+ readonly querySelector: (selector: string) => _Element | null;
17
+ readonly replaceChildren: (...nodes: readonly _Element[]) => void;
18
+ readonly append: (node: _Element) => void;
19
+ };
20
+ export type _Document = {
21
+ defaultView: _View | null;
22
+ readonly createElement: (tag: string) => _Element;
23
+ };
24
+ export type _View = {
25
+ events: readonly CustomEvent[];
26
+ readonly dispatchEvent: (event: Event) => boolean;
27
+ fjsBrowserTestReport?: Promise<unknown>;
28
+ };
29
+ export declare const proof: {
30
+ namedThrow: () => Promise<void>;
31
+ path: () => Promise<void>;
32
+ arbitraryThrow: () => Promise<void>;
33
+ errorFields: () => Promise<void>;
34
+ errorAccessorThrows: () => Promise<void>;
35
+ revokedErrorProxy: () => Promise<void>;
36
+ crossRealmError: () => Promise<void>;
37
+ errorWithoutStack: () => Promise<void>;
38
+ expectedThrow: () => Promise<void>;
39
+ crossRealmPromise: () => Promise<void>;
40
+ spoofedPromiseTag: () => Promise<void>;
41
+ frozenPromiseTag: () => Promise<void>;
42
+ exportedTreeThrows: () => Promise<void>;
43
+ returnedTreeThrows: () => Promise<void>;
44
+ speciesResultIsNotAPromise: () => Promise<void>;
45
+ reportingThrows: () => Promise<void>;
46
+ thenIsATestName: () => Promise<void>;
47
+ batches: () => Promise<void>;
48
+ render: () => Promise<void>;
49
+ renderWithoutView: () => Promise<void>;
50
+ renderReport: () => void;
51
+ sources: () => Promise<void>;
52
+ sourcesProgress: () => Promise<void>;
53
+ sourcesImporterThrows: () => Promise<void>;
54
+ sourcesLoadFailure: () => Promise<void>;
55
+ };