Change to ES6 classes
authorNick Downing <nick@ndcode.org>
Tue, 25 Jan 2022 06:55:38 +0000 (17:55 +1100)
committerNick Downing <nick@ndcode.org>
Tue, 25 Jan 2022 23:51:58 +0000 (10:51 +1100)
Listener.mjs
Resources.mjs
Server.mjs
Site.mjs
cli.mjs

index e94b961..e3cb53e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * Copyright (C) 2018-2022 Nick Downing <nick@ndcode.org>
  * SPDX-License-Identifier: MIT
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -26,85 +26,85 @@ import fsPromises from 'fs/promises'
 import http from 'http'
 import https from 'https'
 
-let Listener = function(request_func, options, diag) {
-  if (!this instanceof Listener)
-    throw new Error('Listener is a constructor')
-  this.request_func = request_func
-  this.options = options
-  this.diag = diag || false
+class Listener {
+  constructor(request_func, options, diag) {
+    this.request_func = request_func
+    this.options = options
+    this.diag = diag || false
 
-  this.server = undefined
-}
+    this.server = undefined
+  }
 
-Listener.prototype.start = async function() {
-  if (this.server === undefined) {
-    if (this.diag)
-      console.log(
-        `start listening on ${this.options.protocol}//${this.options.address}:${this.options.port}`
-      )
-    let server
-    switch (this.options.protocol) {
-    case 'http:':
-      server = http.createServer()
-      break
-    case 'https:':
-      server = https.createServer(
-        {
-          cert: await fsPromises.readFile(this.options.ssl_cert),
-          key: await fsPromises.readFile(this.options.ssl_key)
-        }
-      )
-      break
-    default:
-      assert(false)
-    }
-    server.on(
-      'request',
-      (request, response) => this.request_func(request, response, this)
-        // ignore returned promise
-    )
-    try {
-      await new Promise(
-        (resolve, reject) => {
-          server.on('listening', () => {resolve()})
-          server.on('error', err => {reject(err)})
-          server.listen(this.options.port)
-          // should remove the listeners afterwards
-        }
-      )
-    }
-    catch (err) {
-      if (!(err instanceof Error) || err.code != 'EADDRINUSE')
-        throw err
+  async start() {
+    if (this.server === undefined) {
       if (this.diag)
         console.log(
-          `address ${this.options.protocol}//${this.options.address}:${this.options.port} in use`
+          `start listening on ${this.options.protocol}//${this.options.address}:${this.options.port}`
         )
-      return false
-    }
-    this.server = server
-  }
-  return true
-}
-
-Listener.prototype.stop = async function() {
-  if (this.server !== undefined) {
-    if (this.diag)
-      console.log(
-        `stop listening on ${this.options.protocol}//${this.options.address}:${this.options.port}`
+      let server
+      switch (this.options.protocol) {
+      case 'http:':
+        server = http.createServer()
+        break
+      case 'https:':
+        server = https.createServer(
+          {
+            cert: await fsPromises.readFile(this.options.ssl_cert),
+            key: await fsPromises.readFile(this.options.ssl_key)
+          }
+        )
+        break
+      default:
+        assert(false)
+      }
+      server.on(
+        'request',
+        (request, response) => this.request_func(request, response, this)
+          // ignore returned promise
       )
-    await new Promise(
-      (resolve, reject) => {
-        this.server.close(
-          err => {
-            if (err)
-              reject(err)
-            else
-              resolve()
+      try {
+        await new Promise(
+          (resolve, reject) => {
+            server.on('listening', () => {resolve()})
+            server.on('error', err => {reject(err)})
+            server.listen(this.options.port)
+            // should remove the listeners afterwards
           }
         )
       }
-    )
+      catch (err) {
+        if (!(err instanceof Error) || err.code != 'EADDRINUSE')
+          throw err
+        if (this.diag)
+          console.log(
+            `address ${this.options.protocol}//${this.options.address}:${this.options.port} in use`
+          )
+        return false
+      }
+      this.server = server
+    }
+    return true
+  }
+
+  async stop() {
+    if (this.server !== undefined) {
+      if (this.diag)
+        console.log(
+          `stop listening on ${this.options.protocol}//${this.options.address}:${this.options.port}`
+        )
+      await new Promise(
+        (resolve, reject) => {
+          this.server.close(
+            err => {
+              if (err)
+                reject(err)
+              else
+                resolve()
+            }
+          )
+        }
+      )
+    }
   }
 }
 
index 2fd51a3..410caaf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * Copyright (C) 2018-2022 Nick Downing <nick@ndcode.org>
  * SPDX-License-Identifier: MIT
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
 
 import assert from 'assert'
 
-let Resources = function(diag) {
-  if (!this instanceof Resources)
-    throw new Error('Resources is a constructor')
-  this.map = new Map()
-  this.diag = diag || false
-}
+class Resources {
+  constructor(diag) {
+    this.map = new Map()
+    this.diag = diag || false
+  }
 
-Resources.prototype.ref = async function(key, factory_func, destroy_func) {
-  let result = this.map.get(key)
-  if (result === undefined) {
-    result = {
-      refs: 0,
-      value: factory_func(), // don't await it here
-      destroy_func: destroy_func
+  async ref(key, factory_func, destroy_func) {
+    let result = this.map.get(key)
+    if (result === undefined) {
+      result = {
+        refs: 0,
+        value: factory_func(), // don't await it here
+        destroy_func: destroy_func
+      }
+      this.map.set(key, result)
     }
-    this.map.set(key, result)
-  }
-  result.refs += 1
-  if (this.diag)
-    console.log(`ref ${key} refs -> ${result.refs}`)
-  let value
-  try {
-    value = await result.value
+    result.refs += 1
+    if (this.diag)
+      console.log(`ref ${key} refs -> ${result.refs}`)
+    let value
+    try {
+      value = await result.value
+    }
+    catch (err) {
+      result.refs -= 1
+      console.log(`err ${key} refs-> ${result.refs}`)
+      if (result.refs === 0)
+        this.map.delete(key)
+      throw err
+    }
+    return value
   }
-  catch (err) {
+
+  async unref(key) {
+    result = this.map.get(key)
+    assert(result !== undefined && result.refs > 0)
     result.refs -= 1
-    console.log(`err ${key} refs-> ${result.refs}`)
-    if (result.refs === 0)
+    if (this.diag)
+      console.log(`unref ${key} refs -> ${result.refs}`)
+    if (result.refs === 0) {
       this.map.delete(key)
-    throw err
-  }
-  return value
-}
-
-Resources.prototype.unref = async function(key) {
-  result = this.map.get(key)
-  assert(result !== undefined && result.refs > 0)
-  result.refs -= 1
-  if (this.diag)
-    console.log(`unref ${key} refs -> ${result.refs}`)
-  if (result.refs === 0) {
-    this.map.delete(key)
-    if (result.destroy_func !== undefined)
-      try {
-        await result.destroy_func(await result.value)
-      }
-      catch (err) {
-        console.err(err.stack || err.message)
-      }
+      if (result.destroy_func !== undefined)
+        try {
+          await result.destroy_func(await result.value)
+        }
+        catch (err) {
+          console.error(err.stack || err.message)
+        }
+    }
   }
 }
 
index 1612bd5..baf64a3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * Copyright (C) 2018-2022 Nick Downing <nick@ndcode.org>
  * SPDX-License-Identifier: MIT
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -29,183 +29,182 @@ import assert from 'assert'
 import jst_server from './index.mjs'
 import url from 'url'
 
-let Server = function(resources, options/*, prev_server*/) {
-  if (!this instanceof Server)
-    throw new Error('Server is a constructor')
-  this.resources = resources
-  this.options = Object.assign(
-    {
-      caching: false,
-      listeners: [
-        {
-          protocol: 'http:'
-        },
-        {
-          protocol: 'https:'
-        }
-      ],
-      hosts: {
-        'localhost': {
-          type: 'site',
-          root: 'site'
-        },
-        'localhost.localdomain': {
-          type: 'redirect',
-          host: 'localhost'
-        }
-      }
-    }, 
-    options || {}
-  )
-
-  this.jst_cache = undefined
-  this.site_roots = undefined
-  this.listeners = undefined
-  this.request_func =
-    (request, response, listener) =>
-    /*await*/ this.respond(request, response, listener.options.protocol)
-    // returned Promise will be ignored
-}
-
-Server.prototype.start = async function() {
-  assert(this.jst_cache === undefined)
-  this.jst_cache = await this.resources.ref(
-    'jst_cache:.',
-    async () => new JSTCache('.', {_jst_server: jst_server}, true)
-  )
-
-  assert(this.site_roots === undefined)
-  this.site_roots = {}
-  for (let i in this.options.hosts) {
-    let host
-    if (
-      Object.prototype.hasOwnProperty.call(this.options.hosts, i) &&
-      (host = this.options.hosts[i]).type === 'site' &&
-      !Object.prototype.hasOwnProperty.call(this.site_roots, host.root)
-    )
-      this.site_roots[host.root] = await this.resources.ref(
-        `site_root:${host.root}`,
-        async () => (
+class Server {
+  constructor(resources, options/*, prev_server*/) {
+    this.resources = resources
+    this.options = Object.assign(
+      {
+        caching: false,
+        listeners: [
           {
-            jst_cache: await this.resources.ref(
-              `jst_cache:${host.root}`,
-              async () =>
-                new JSTCache(host.root, {_jst_server: jst_server}, true)
-            ),
-            root: host.root,
-            site: undefined
+            protocol: 'http:'
+          },
+          {
+            protocol: 'https:'
+          }
+        ],
+        hosts: {
+          'localhost': {
+            type: 'site',
+            root: 'site'
+          },
+          'localhost.localdomain': {
+            type: 'redirect',
+            host: 'localhost'
           }
-        ),
-        async site_root => {
-          await this.resources.unref(`jst_cache:${site_root.root}`)
-          if (site_root.site !== undefined)
-            await site_root.site.stop()
         }
-      )
+      },
+      options || {}
+    )
+
+    this.jst_cache = undefined
+    this.site_roots = undefined
+    this.listeners = undefined
+    this.request_func =
+      (request, response, listener) =>
+      /*await*/ this.respond(request, response, listener.options.protocol)
+      // returned Promise will be ignored
   }
 
-  assert(this.listeners === undefined)
-  this.listeners = []
-  for (let i = 0; i < this.options.listeners.length; ++i) {
-    let options
-    switch (this.options.listeners[i].protocol) {
-    case 'http:':
-      options = Object.assign(
-        {
-          protocol: 'https:',
-          address: '0.0.0.0',
-          port: 8080
-        },
-        this.options.listeners[i]
+  async start() {
+    assert(this.jst_cache === undefined)
+    this.jst_cache = await this.resources.ref(
+      'jst_cache:.',
+      async () => new JSTCache('.', {_jst_server: jst_server}, true)
+    )
+
+    assert(this.site_roots === undefined)
+    this.site_roots = {}
+    for (let i in this.options.hosts) {
+      let host
+      if (
+        Object.prototype.hasOwnProperty.call(this.options.hosts, i) &&
+        (host = this.options.hosts[i]).type === 'site' &&
+        !Object.prototype.hasOwnProperty.call(this.site_roots, host.root)
       )
-      break
-    case 'https:':
-      options = Object.assign(
-        {
-          protocol: 'https:',
-          address: '0.0.0.0',
-          port: 8443,
-          ssl_cert: '_ssl/localhost_cert_bundle.pem',
-          ssl_key: '_ssl/localhost_key.pem'
-        },
-        this.options.listeners[i]
+        this.site_roots[host.root] = await this.resources.ref(
+          `site_root:${host.root}`,
+          async () => (
+            {
+              jst_cache: await this.resources.ref(
+                `jst_cache:${host.root}`,
+                async () =>
+                  new JSTCache(host.root, {_jst_server: jst_server}, true)
+              ),
+              root: host.root,
+              site: undefined
+            }
+          ),
+          async site_root => {
+            await this.resources.unref(`jst_cache:${site_root.root}`)
+            if (site_root.site !== undefined)
+              await site_root.site.stop()
+          }
+        )
+    }
+
+    assert(this.listeners === undefined)
+    this.listeners = []
+    for (let i = 0; i < this.options.listeners.length; ++i) {
+      let options
+      switch (this.options.listeners[i].protocol) {
+      case 'http:':
+        options = Object.assign(
+          {
+            protocol: 'https:',
+            address: '0.0.0.0',
+            port: 8080
+          },
+          this.options.listeners[i]
+        )
+        break
+      case 'https:':
+        options = Object.assign(
+          {
+            protocol: 'https:',
+            address: '0.0.0.0',
+            port: 8443,
+            ssl_cert: '_ssl/localhost_cert_bundle.pem',
+            ssl_key: '_ssl/localhost_key.pem'
+          },
+          this.options.listeners[i]
+        )
+        break
+      default:
+        assert(false)
+      }
+      let listener = await this.resources.ref(
+        `listener:${JSON.stringify(options)}`,
+        async () => new Listener(undefined, options, true),
+        listener => /*await*/ listener.stop()
       )
-      break
-    default:
-      assert(false)
+      this.listeners.push(listener)
     }
-    let listener = await this.resources.ref(
-      `listener:${JSON.stringify(options)}`,
-      async () => new Listener(undefined, options, true),
-      listener => /*await*/ listener.stop()
-    )
-    this.listeners.push(listener)
   }
-}
 
-Server.prototype.stop = async function() {
-  assert(this.jst_cache !== undefined)
-  await this.resources.unref('jst_cache:.')
+  async stop() {
+    assert(this.jst_cache !== undefined)
+    await this.resources.unref('jst_cache:.')
 
-  assert(this.site_roots !== undefined)
-  for (let i in this.site_roots)
-    if (Object.prototype.hasOwnProperty.call(this.site_roots, i))
-      await this.resources.unref(`site_root:${i}`)
+    assert(this.site_roots !== undefined)
+    for (let i in this.site_roots)
+      if (Object.prototype.hasOwnProperty.call(this.site_roots, i))
+        await this.resources.unref(`site_root:${i}`)
 
-  assert(this.listeners !== undefined)
-  for (let i = 0; i < this.listeners.length; ++i)
-    await this.resources.unref(
-      `listener:${JSON.stringify(this.listeners[i].options)}`
-    )
-}
-Server.prototype.kick = async function() {
-  assert(this.jst_cache !== undefined)
-  this.jst_cache.kick()
-  assert(this.site_roots !== undefined)
-  for (let i in this.site_roots)
-    if (Object.prototype.hasOwnProperty.call(this.site_roots, i)) {
-      let site_root = this.site_roots[i]
-      let config = await site_root.jst_cache.get(
-        `${i}/_config/site.jst`,
-        true
+    assert(this.listeners !== undefined)
+    for (let i = 0; i < this.listeners.length; ++i)
+      await this.resources.unref(
+        `listener:${JSON.stringify(this.listeners[i].options)}`
       )
-      if (config !== undefined) {
-        let prev_site = site_root.site
-        let new_site = await config(this.resources, i, prev_site)
-        await new_site.start() // exception here cancels site change
-        site_root.site = new_site
-        if (prev_site !== undefined)
-          await prev_site.stop() // exception here doesn't cancel change
+  }
+
+  async kick() {
+    assert(this.jst_cache !== undefined)
+    this.jst_cache.kick()
+
+    assert(this.site_roots !== undefined)
+    for (let i in this.site_roots)
+      if (Object.prototype.hasOwnProperty.call(this.site_roots, i)) {
+        let site_root = this.site_roots[i]
+        let config = await site_root.jst_cache.get(
+          `${i}/_config/site.jst`,
+          true
+        )
+        if (config !== undefined) {
+          let prev_site = site_root.site
+          let new_site = await config(this.resources, i, prev_site)
+          await new_site.start() // exception here cancels site change
+          site_root.site = new_site
+          if (prev_site !== undefined)
+            await prev_site.stop() // exception here doesn't cancel change
+        }
+        await site_root.site.kick()
       }
-      await site_root.site.kick()
-    }
 
-  assert(this.listeners !== undefined)
-  for (let i = 0; i < this.listeners.length; ++i) {
-    this.listeners[i].request_func = this.request_func
-    await this.listeners[i].start()
+    assert(this.listeners !== undefined)
+    for (let i = 0; i < this.listeners.length; ++i) {
+      this.listeners[i].request_func = this.request_func
+      await this.listeners[i].start()
+    }
   }
-}
 
-Server.prototype.serve_internal = function(response, status, mime_type, data) {
-  response.statusCode = status
-  // no real need to cache errors and hostname redirects
-  // (pathname redirects on the other hand will be cached, see Site.js)
-  response.setHeader('Content-Type', mime_type)
-  response.setHeader('Content-Length', data.length)
-  response.end(data)
-}
+  serve_internal(response, status, mime_type, data) {
+    response.statusCode = status
+    // no real need to cache errors and hostname redirects
+    // (pathname redirects on the other hand will be cached, see Site.js)
+    response.setHeader('Content-Type', mime_type)
+    response.setHeader('Content-Length', data.length)
+    response.end(data)
+  }
 
-Server.prototype.die = function(response, pathname, message) {
-  console.log(message)
-  this.serve_internal(
-    response,
-    404,
-    'text/html; charset=utf-8',
-    Buffer.from(
-      `<html>
+  die(response, pathname, message) {
+    console.log(message)
+    this.serve_internal(
+      response,
+      404,
+      'text/html; charset=utf-8',
+      Buffer.from(
+        `<html>
   <head>
     <meta http-equiv="content-type" content="text/html;charset=utf-8">
     <title>404 Not Found</title>
@@ -216,20 +215,20 @@ Server.prototype.die = function(response, pathname, message) {
   </body>
 </html>
 `
+      )
     )
-  )
-}
+  }
 
-Server.prototype.redirect = function(response, location, message) {
-  console.log(message)
-  response.statusCode = 301
-  response.setHeader('Location', location)
-  this.serve_internal(
-    response,
-    301,
-    'text/html; charset=utf-8',
-    Buffer.from(
-      `<html>
+  redirect(response, location, message) {
+    console.log(message)
+    response.statusCode = 301
+    response.setHeader('Location', location)
+    this.serve_internal(
+      response,
+      301,
+      'text/html; charset=utf-8',
+      Buffer.from(
+        `<html>
   <head>
     <meta http-equiv="content-type" content="text/html;charset=utf-8">
     <title>301 Moved Permanently</title>
@@ -240,64 +239,65 @@ Server.prototype.redirect = function(response, location, message) {
   </body>
 </html>
 `
+      )
     )
-  )
-}
-
-Server.prototype.respond = async function(request, response, protocol) {
-  try {
-    let parsed_url = url.parse(
-      protocol + '//' + (request.headers.host || 'localhost') + request.url,
-      true
-    )
-    //console.log('parsed_url', parsed_url)
+  }
 
-    if (
-      !Object.prototype.hasOwnProperty.call(this.options.hosts, parsed_url.hostname)
-    ) {
-      this.die(
-        response,
-        parsed_url.pathname,
-        'nonexistent site: ' + parsed_url.hostname
+  async respond(request, response, protocol) {
+    try {
+      let parsed_url = url.parse(
+        protocol + '//' + (request.headers.host || 'localhost') + request.url,
+        true
       )
-      return
-    }
+      //console.log('parsed_url', parsed_url)
 
-    let host = this.options.hosts[parsed_url.hostname]
-    switch (host.type) {
-    case 'redirect':
-      let new_host = host.host
-      if (parsed_url.port !== null)
-        new_host += ':' + parsed_url.port
-      this.redirect(
+      if (
+        !Object.prototype.hasOwnProperty.call(this.options.hosts, parsed_url.hostname)
+      ) {
+        this.die(
+          response,
+          parsed_url.pathname,
+          'nonexistent site: ' + parsed_url.hostname
+        )
+        return
+      }
+
+      let host = this.options.hosts[parsed_url.hostname]
+      switch (host.type) {
+      case 'redirect':
+        let new_host = host.host
+        if (parsed_url.port !== null)
+          new_host += ':' + parsed_url.port
+        this.redirect(
+          response,
+          `${parsed_url.protocol}//${new_host}${request.url}`,
+          `redirecting ${parsed_url.host} to ${new_host}`
+        )
+        break
+      case 'site':
+        await this.site_roots[host.root].site.respond(
+          {
+            parsed_url: parsed_url,
+            response: response,
+            request: request,
+            server: this
+          }
+        )
+        break
+      default:
+        assert(false)
+      }
+    }
+    catch (err) {
+      let message = (err.stack || err.message).toString()
+      console.error(message)
+      this.serve_internal(
         response,
-        `${parsed_url.protocol}//${new_host}${request.url}`,
-        `redirecting ${parsed_url.host} to ${new_host}`
-      )
-      break
-    case 'site':
-      await this.site_roots[host.root].site.respond(
-        {
-          parsed_url: parsed_url,
-          response: response,
-          request: request,
-          server: this
-        }
+        500,
+        'text/html; charset=utf-8',
+        Buffer.from(`<html><body><pre>${message}</pre></body></html>`)
       )
-      break
-    default:
-      assert(false)
     }
-  }     
-  catch (err) {
-    let message = (err.stack || err.message).toString()
-    console.error(message)
-    this.serve_internal(
-      response,
-      500,
-      'text/html; charset=utf-8',
-      Buffer.from(`<html><body><pre>${message}</pre></body></html>`)
-    )
   }
 }
 
index 05ff9c6..8b267de 100644 (file)
--- a/Site.mjs
+++ b/Site.mjs
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2021 Nick Downing <nick@ndcode.org>
+ * Copyright (C) 2018-2022 Nick Downing <nick@ndcode.org>
  * SPDX-License-Identifier: MIT
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -39,229 +39,228 @@ import fs from 'fs'
 import fsPromises from 'fs/promises'
 import jst_server from './index.mjs'
 
-let Site = function(resources, root, options/*, prev_site*/) {
-  if (!this instanceof Site)
-    throw new Error('Site is a constructor')
-  this.resources = resources
-  this.root = root
-  this.options = Object.assign(
-    {
-      caching: false,
-      mime_types: {
-        '.css': 'text/css; charset=utf-8',
-        '.html': 'text/html; charset=utf-8',
-        '.ico': 'image/x-icon',
-        '.jpg': 'image/jpeg',
-        '.jpeg': 'image/jpeg',
-        '.js': 'application/javascript; charset=utf-8',
-        '.json': 'application/json; charset=utf-8',
-        '.mp4': 'video/mp4',
-        '.pdf': 'application/pdf',
-        '.png': 'image/png',
-        '.svg': 'image/svg+xml',
-        '.ttf': 'application/octet-stream',
-        '.woff': 'font/woff',
-        '.woff2': 'font/woff2',
-        '.xml': 'text/xml; charset=utf-8'
+class Site {
+  constructor(resources, root, options/*, prev_site*/) {
+    this.resources = resources
+    this.root = root
+    this.options = Object.assign(
+      {
+        caching: false,
+        mime_types: {
+          '.css': 'text/css; charset=utf-8',
+          '.html': 'text/html; charset=utf-8',
+          '.ico': 'image/x-icon',
+          '.jpg': 'image/jpeg',
+          '.jpeg': 'image/jpeg',
+          '.js': 'application/javascript; charset=utf-8',
+          '.json': 'application/json; charset=utf-8',
+          '.mp4': 'video/mp4',
+          '.pdf': 'application/pdf',
+          '.png': 'image/png',
+          '.svg': 'image/svg+xml',
+          '.ttf': 'application/octet-stream',
+          '.woff': 'font/woff',
+          '.woff2': 'font/woff2',
+          '.xml': 'text/xml; charset=utf-8'
+        },
+        certbot_webroot: '/var/www/html'
       },
-      certbot_webroot: '/var/www/html'
-    },
-    options || {}
-  )
-
-  this.json_cache = undefined
-  this.json_cache_rw = undefined
-  this.jst_cache = undefined
-  this.less_css_cache = undefined
-  this.menu_cache = undefined
-  this.min_css_cache = undefined
-  this.min_js_cache = undefined
-  this.min_html_cache = undefined
-  this.min_svg_cache = undefined
-  this.text_cache = undefined
-  this.zip_cache = undefined
-
-  this.socket_io_connect_listeners = [] // later will use this for destruction
-}
+      options || {}
+    )
 
-Site.prototype.start = async function() {
-  assert(this.json_cache === undefined)
-  this.json_cache = await this.resources.ref(
-    'json_cache',
-    async () => new JSONCache(true)
-  )
-
-  assert(this.json_cache_rw === undefined)
-  this.json_cache_rw = await this.resources.ref(
-    'json_cache_rw',
-    async () => new JSONCacheRW(true)
-  )
-
-  assert(this.jst_cache === undefined)
-  this.jst_cache = await this.resources.ref(
-    `jst_cache:${this.root}`,
-    async () => new JSTCache(this.root, {_jst_server: jst_server}, true)
-  )
-
-  assert(this.less_css_cache === undefined)
-  this.less_css_cache = await this.resources.ref(
-    `less_css_cache:${this.root}`,
-    async () => new LessCSSCache(this.root, true)
-  )
-
-  assert(this.menu_cache === undefined)
-  this.menu_cache = await this.resources.ref(
-    'menu_cache',
-    async () => new MenuCache(true)
-  )
-
-  assert(this.min_css_cache === undefined)
-  this.min_css_cache = await this.resources.ref(
-    'min_css_cache',
-    async () => new MinCSSCache(true)
-  )
-
-  assert(this.min_js_cache === undefined)
-  this.min_js_cache = await this.resources.ref(
-    'min_js_cache',
-    async () => new MinJSCache(true)
-  )
-
-  assert(this.min_html_cache === undefined)
-  this.min_html_cache = await this.resources.ref(
-    'min_html_cache',
-    async () => new MinHTMLCache(true)
-  )
-
-  assert(this.min_svg_cache === undefined)
-  this.min_svg_cache = await this.resources.ref(
-    'min_svg_cache',
-    async () => new MinSVGCache(true)
-  )
-
-  assert(this.sass_css_cache === undefined)
-  this.sass_css_cache = await this.resources.ref(
-    `sass_css_cache:${this.root}`,
-    async () => new SassCSSCache(this.root, true)
-  )
-
-  assert(this.text_cache === undefined)
-  this.text_cache = await this.resources.ref(
-    'text_cache',
-    async () => new TextCache(true)
-  )
-
-  assert(this.zip_cache === undefined)
-  this.zip_cache = await this.resources.ref(
-    'zip_cache',
-    async () => new ZipCache(true)
-  )
-}
+    this.json_cache = undefined
+    this.json_cache_rw = undefined
+    this.jst_cache = undefined
+    this.less_css_cache = undefined
+    this.menu_cache = undefined
+    this.min_css_cache = undefined
+    this.min_js_cache = undefined
+    this.min_html_cache = undefined
+    this.min_svg_cache = undefined
+    this.text_cache = undefined
+    this.zip_cache = undefined
+
+    this.socket_io_connect_listeners = [] // later will use this for destruction
+  }
 
-Site.prototype.stop = async function() {
-  assert(this.json_cache !== undefined)
-  await this.resources.unref('json_cache')
+  async start() {
+    assert(this.json_cache === undefined)
+    this.json_cache = await this.resources.ref(
+      'json_cache',
+      async () => new JSONCache(true)
+    )
 
-  assert(this.json_cache_rw !== undefined)
-  await this.resources.unref('json_cache_rw')
+    assert(this.json_cache_rw === undefined)
+    this.json_cache_rw = await this.resources.ref(
+      'json_cache_rw',
+      async () => new JSONCacheRW(true)
+    )
 
-  assert(this.jst_cache !== undefined)
-  await this.resources.unref(`jst_cache:${this.root}`)
+    assert(this.jst_cache === undefined)
+    this.jst_cache = await this.resources.ref(
+      `jst_cache:${this.root}`,
+      async () => new JSTCache(this.root, {_jst_server: jst_server}, true)
+    )
 
-  assert(this.less_css_cache !== undefined)
-  await this.resources.unref(`less_css_cache:${this.root}`)
+    assert(this.less_css_cache === undefined)
+    this.less_css_cache = await this.resources.ref(
+      `less_css_cache:${this.root}`,
+      async () => new LessCSSCache(this.root, true)
+    )
 
-  assert(this.menu_cache !== undefined)
-  await this.resources.unref('menu_cache')
+    assert(this.menu_cache === undefined)
+    this.menu_cache = await this.resources.ref(
+      'menu_cache',
+      async () => new MenuCache(true)
+    )
 
-  assert(this.min_css_cache !== undefined)
-  await this.resources.unref('min_css_cache')
+    assert(this.min_css_cache === undefined)
+    this.min_css_cache = await this.resources.ref(
+      'min_css_cache',
+      async () => new MinCSSCache(true)
+    )
 
-  assert(this.min_js_cache !== undefined)
-  await this.resources.unref('min_js_cache')
+    assert(this.min_js_cache === undefined)
+    this.min_js_cache = await this.resources.ref(
+      'min_js_cache',
+      async () => new MinJSCache(true)
+    )
 
-  assert(this.min_html_cache !== undefined)
-  await this.resources.unref('min_html_cache')
+    assert(this.min_html_cache === undefined)
+    this.min_html_cache = await this.resources.ref(
+      'min_html_cache',
+      async () => new MinHTMLCache(true)
+    )
 
-  assert(this.min_svg_cache !== undefined)
-  await this.resources.unref('min_svg_cache')
+    assert(this.min_svg_cache === undefined)
+    this.min_svg_cache = await this.resources.ref(
+      'min_svg_cache',
+      async () => new MinSVGCache(true)
+    )
 
-  assert(this.sass_css_cache !== undefined)
-  await this.resources.unref(`sass_css_cache:${this.root}`)
+    assert(this.sass_css_cache === undefined)
+    this.sass_css_cache = await this.resources.ref(
+      `sass_css_cache:${this.root}`,
+      async () => new SassCSSCache(this.root, true)
+    )
 
-  assert(this.text_cache !== undefined)
-  await this.resources.unref('text_cache')
+    assert(this.text_cache === undefined)
+    this.text_cache = await this.resources.ref(
+      'text_cache',
+      async () => new TextCache(true)
+    )
 
-  assert(this.zip_cache !== undefined)
-  await this.resources.unref('zip_cache')
-}
+    assert(this.zip_cache === undefined)
+    this.zip_cache = await this.resources.ref(
+      'zip_cache',
+      async () => new ZipCache(true)
+    )
+  }
 
-Site.prototype.kick = async function() {
-  assert(this.json_cache !== undefined)
-  this.json_cache.kick()
+  async stop() {
+    assert(this.json_cache !== undefined)
+    await this.resources.unref('json_cache')
 
-  assert(this.json_cache_rw !== undefined)
-  this.json_cache_rw.kick()
+    assert(this.json_cache_rw !== undefined)
+    await this.resources.unref('json_cache_rw')
 
-  assert(this.jst_cache !== undefined)
-  this.jst_cache.kick()
+    assert(this.jst_cache !== undefined)
+    await this.resources.unref(`jst_cache:${this.root}`)
 
-  assert(this.less_css_cache !== undefined)
-  this.less_css_cache.kick()
+    assert(this.less_css_cache !== undefined)
+    await this.resources.unref(`less_css_cache:${this.root}`)
 
-  assert(this.menu_cache !== undefined)
-  this.menu_cache.kick()
+    assert(this.menu_cache !== undefined)
+    await this.resources.unref('menu_cache')
 
-  assert(this.min_css_cache !== undefined)
-  this.min_css_cache.kick()
+    assert(this.min_css_cache !== undefined)
+    await this.resources.unref('min_css_cache')
 
-  assert(this.min_js_cache !== undefined)
-  this.min_js_cache.kick()
+    assert(this.min_js_cache !== undefined)
+    await this.resources.unref('min_js_cache')
 
-  assert(this.min_html_cache !== undefined)
-  this.min_html_cache.kick()
+    assert(this.min_html_cache !== undefined)
+    await this.resources.unref('min_html_cache')
 
-  assert(this.min_svg_cache !== undefined)
-  this.min_svg_cache.kick()
+    assert(this.min_svg_cache !== undefined)
+    await this.resources.unref('min_svg_cache')
 
-  assert(this.sass_css_cache !== undefined)
-  this.sass_css_cache.kick()
+    assert(this.sass_css_cache !== undefined)
+    await this.resources.unref(`sass_css_cache:${this.root}`)
 
-  assert(this.text_cache !== undefined)
-  this.text_cache.kick()
+    assert(this.text_cache !== undefined)
+    await this.resources.unref('text_cache')
 
-  assert(this.zip_cache !== undefined)
-  this.zip_cache.kick()
-}
+    assert(this.zip_cache !== undefined)
+    await this.resources.unref('zip_cache')
+  }
 
-Site.prototype.serve_internal = function(response, status, mime_type, caching, data) {
-  response.statusCode = status
-  // html files will be direct recipient of links/bookmarks so can't have
-  // a long lifetime, other files like css or images are often large files
-  // and won't change frequently (but we'll need cache busting eventually)
-  if (caching && mime_type !== this.options.mime_type['.html'])
-    response.setHeader('Cache-Control', 'max-age=3600')
-  response.setHeader('Content-Type', mime_type)
-  response.setHeader('Content-Length', data.length)
-  response.end(data)
-}
+  async kick() {
+    assert(this.json_cache !== undefined)
+    this.json_cache.kick()
 
-Site.prototype.serve = function(env, status, data, from) {
-  console.log(
-    `${env.parsed_url.host} serving ${env.parsed_url.pathname} size ${data.length} from ${from}`
-  )
-  this.serve_internal(env.response, status, env.mime_type, env.caching, data)
-}
+    assert(this.json_cache_rw !== undefined)
+    this.json_cache_rw.kick()
+
+    assert(this.jst_cache !== undefined)
+    this.jst_cache.kick()
+
+    assert(this.less_css_cache !== undefined)
+    this.less_css_cache.kick()
+
+    assert(this.menu_cache !== undefined)
+    this.menu_cache.kick()
+
+    assert(this.min_css_cache !== undefined)
+    this.min_css_cache.kick()
+
+    assert(this.min_js_cache !== undefined)
+    this.min_js_cache.kick()
+
+    assert(this.min_html_cache !== undefined)
+    this.min_html_cache.kick()
+
+    assert(this.min_svg_cache !== undefined)
+    this.min_svg_cache.kick()
+
+    assert(this.sass_css_cache !== undefined)
+    this.sass_css_cache.kick()
+
+    assert(this.text_cache !== undefined)
+    this.text_cache.kick()
 
-Site.prototype.die = function(env, message) {
-  console.log(`${env.parsed_url.host} ${message}`)
-  this.serve_internal(
-    env.response,
-    404,
-    this.options.mime_types['.html'],
-    false,
-    Buffer.from(
-      `<html>
+    assert(this.zip_cache !== undefined)
+    this.zip_cache.kick()
+  }
+
+  serve_internal(response, status, mime_type, caching, data) {
+    response.statusCode = status
+    // html files will be direct recipient of links/bookmarks so can't have
+    // a long lifetime, other files like css or images are often large files
+    // and won't change frequently (but we'll need cache busting eventually)
+    if (caching && mime_type !== this.options.mime_type['.html'])
+      response.setHeader('Cache-Control', 'max-age=3600')
+    response.setHeader('Content-Type', mime_type)
+    response.setHeader('Content-Length', data.length)
+    response.end(data)
+  }
+
+  serve(env, status, data, from) {
+    console.log(
+      `${env.parsed_url.host} serving ${env.parsed_url.pathname} size ${data.length} from ${from}`
+    )
+    this.serve_internal(env.response, status, env.mime_type, env.caching, data)
+  }
+
+  die(env, message) {
+    console.log(`${env.parsed_url.host} ${message}`)
+    this.serve_internal(
+      env.response,
+      404,
+      this.options.mime_types['.html'],
+      false,
+      Buffer.from(
+        `<html>
   <head>
     <meta http-equiv="content-type" content="text/html;charset=utf-8">
     <title>404 Not Found</title>
@@ -272,24 +271,24 @@ Site.prototype.die = function(env, message) {
   </body>
 </html>
 `
+      )
     )
-  )
-}
+  }
 
-Site.prototype.redirect = function(env, pathname, message) {
-  console.log(
-    `${env.parsed_url.host} redirecting ${env.parsed_url.pathname} to ${pathname}`
-  )
-  let location = pathname + (env.parsed_url.search || '')
-  env.response.statusCode = 301
-  env.response.setHeader('Location', location)
-  this.serve_internal(
-    env.response,
-    301,
-    this.options.mime_types['.html'],
-    false,
-    Buffer.from(
-      `<html>
+  redirect(env, pathname, message) {
+    console.log(
+      `${env.parsed_url.host} redirecting ${env.parsed_url.pathname} to ${pathname}`
+    )
+    let location = pathname + (env.parsed_url.search || '')
+    env.response.statusCode = 301
+    env.response.setHeader('Location', location)
+    this.serve_internal(
+      env.response,
+      301,
+      this.options.mime_types['.html'],
+      false,
+      Buffer.from(
+        `<html>
   <head>
     <meta http-equiv="content-type" content="text/html;charset=utf-8">
     <title>301 Moved Permanently</title>
@@ -300,88 +299,87 @@ Site.prototype.redirect = function(env, pathname, message) {
   </body>
 </html>
 `
+      )
     )
-  )
-}
-
-Site.prototype.internal_ensure_dir = async function(pathname) {
-  try {
-    await fsPromises.mkdir(pathname)
-    console.log('create directory', pathname)
   }
-  catch (err) {
-    if (err.code !== 'EEXIST') // should check error type
-      throw err
+
+  async internal_ensure_dir(pathname) {
+    try {
+      await fsPromises.mkdir(pathname)
+      console.log('create directory', pathname)
+    }
+    catch (err) {
+      if (err.code !== 'EEXIST') // should check error type
+        throw err
+    }
   }
-}
 
-Site.prototype.get_json = function(pathname) {
-  return /*await*/ this.json_cache.get(this.root + pathname)
-}
+  get_json(pathname) {
+    return /*await*/ this.json_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_jst = function(pathname) {
-  return /*await*/ this.jst_cache.get(this.root + pathname)
-}
+  get_jst(pathname) {
+    return /*await*/ this.jst_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_less_css = function(pathname) {
-  return /*await*/ this.less_css_cache.get(this.root + pathname)
-}
+  get_less_css(pathname) {
+    return /*await*/ this.less_css_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_menu = function(pathname) {
-  return /*await*/ this.menu_cache.get(this.root + pathname)
-}
+  get_menu(pathname) {
+    return /*await*/ this.menu_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_min_css = function(pathname) {
-  return /*await*/ this.min_css_cache.get(this.root + pathname)
-}
+  get_min_css(pathname) {
+    return /*await*/ this.min_css_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_min_html = function(pathname) {
-  return /*await*/ this.min_html_cache.get(this.root + pathname)
-}
+  get_min_html(pathname) {
+    return /*await*/ this.min_html_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_min_js = function(pathname) {
-  return /*await*/ this.min_js_cache.get(this.root + pathname)
-}
+  get_min_js(pathname) {
+    return /*await*/ this.min_js_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_min_svg = function(pathname) {
-  return /*await*/ this.min_svg_cache.get(this.root + pathname)
-}
+  get_min_svg(pathname) {
+    return /*await*/ this.min_svg_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_sass_css = function(pathname) {
-  return /*await*/ this.sass_css_cache.get(this.root + pathname)
-}
+  get_sass_css(pathname) {
+    return /*await*/ this.sass_css_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_text = function(pathname) {
-  return /*await*/ this.text_cache.get(this.root + pathname)
-}
+  get_text(pathname) {
+    return /*await*/ this.text_cache.get(this.root + pathname)
+  }
 
-Site.prototype.get_zip = function(pathname) {
-  return /*await*/ this.zip_cache.get(this.root + pathname)
-}
+  get_zip(pathname) {
+    return /*await*/ this.zip_cache.get(this.root + pathname)
+  }
 
-Site.prototype.ensure_dir = async function(pathname) {
-  return /*await*/ this.internal_ensure_dir(this.root + pathname)
-}
+  async ensure_dir(pathname) {
+    return /*await*/ this.internal_ensure_dir(this.root + pathname)
+  }
 
-// this is for read/write JSON files
-// they will not be reloaded from disk if modified
-Site.prototype.read_json = async function(pathname, default_value) {
-  return /*await*/ this.json_cache_rw.read(
-    this.root + pathname,
-    default_value
-  )
-}
+  // this is for read/write JSON files
+  // they will not be reloaded from disk if modified
+  async read_json(pathname, default_value) {
+    return /*await*/ this.json_cache_rw.read(
+      this.root + pathname,
+      default_value
+    )
+  }
 
-Site.prototype.write_json = async function(pathname, value, timeout) {
-  return /*await*/ this.json_cache_rw.write(
-    this.root + pathname,
-    value,
-    timeout
-  )
-}
+  async write_json(pathname, value, timeout) {
+    return /*await*/ this.json_cache_rw.write(
+      this.root + pathname,
+      value,
+      timeout
+    )
+  }
 
-Site.prototype.modify_json =
-  async function(pathname, default_value, modify_func, timeout) {
+  async modify_json(pathname, default_value, modify_func, timeout) {
     return /*await*/ this.json_cache_rw.modify(
       this.root + pathname,
       default_value,
@@ -390,362 +388,363 @@ Site.prototype.modify_json =
     )
   }
 
-Site.prototype.serve_jst = async function(env, pathname, ...args) {
-  let template
-  try {
-    template = await this.jst_cache.get(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
-  }
-  env.site = this
-  await template(env, ...args)
-  return true
-}
-
-Site.prototype.serve_less_css = async function(env, pathname) {
-  if (pathname.slice(-9) !== '.css.less')
-    return false
-
-  let data
-  try {
-    data = await this.less_css_cache.get(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
+  async serve_jst(env, pathname, ...args) {
+    let template
+    try {
+      template = await this.jst_cache.get(pathname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      return false
+    }
+    env.site = this
+    await template(env, ...args)
+    return true
   }
-  this.serve(env, 200, data, 'less_css')
-  return true
-}
 
-Site.prototype.serve_min_css = async function(env, pathname) {
-  if (pathname.slice(-8) !== '.css.min')
-    return false
+  async serve_less_css(env, pathname) {
+    if (pathname.slice(-9) !== '.css.less')
+      return false
 
-  let data
-  try {
-    data = await this.min_css_cache.get(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
+    let data
+    try {
+      data = await this.less_css_cache.get(pathname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      return false
+    }
+    this.serve(env, 200, data, 'less_css')
+    return true
   }
-  this.serve(env, 200, data, 'min_css')
-  return true
-}
 
-Site.prototype.serve_min_html = async function(env, pathname) {
-  if (pathname.slice(-9) !== '.html.min')
-    return false
+  async serve_min_css(env, pathname) {
+    if (pathname.slice(-8) !== '.css.min')
+      return false
 
-  let data
-  try {
-    data = await this.min_html_cache.get(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
+    let data
+    try {
+      data = await this.min_css_cache.get(pathname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      return false
+    }
+    this.serve(env, 200, data, 'min_css')
+    return true
   }
-  this.serve(env, 200, data, 'min_html')
-  return true
-}
 
-Site.prototype.serve_min_js = async function(env, pathname) {
-  if (pathname.slice(-7) !== '.js.min')
-    return false
+  async serve_min_html(env, pathname) {
+    if (pathname.slice(-9) !== '.html.min')
+      return false
 
-  let data
-  try {
-    data = await this.min_js_cache.get(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
+    let data
+    try {
+      data = await this.min_html_cache.get(pathname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      return false
+    }
+    this.serve(env, 200, data, 'min_html')
+    return true
   }
-  this.serve(env, 200, data, 'min_js')
-  return true
-}
 
-Site.prototype.serve_min_svg = async function(env, pathname) {
-  if (pathname.slice(-8) !== '.svg.min')
-    return false
+  async serve_min_js(env, pathname) {
+    if (pathname.slice(-7) !== '.js.min')
+      return false
 
-  let data
-  try {
-    data = await this.min_svg_cache.get(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
+    let data
+    try {
+      data = await this.min_js_cache.get(pathname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      return false
+    }
+    this.serve(env, 200, data, 'min_js')
+    return true
   }
-  this.serve(env, 200, data, 'min_svg')
-  return true
-}
-
-Site.prototype.serve_sass_css = async function(env, pathname) {
-  if (
-    pathname.slice(-9) !== '.css.sass' &&
-      pathname.slice(-9) !== '.css.scss'
-  )
-    return false
 
-  let data
-  try {
-    data = await this.sass_css_cache.get(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
-  }
-  this.serve(env, 200, data, 'sass_css')
-  return true
-}
+  async serve_min_svg(env, pathname) {
+    if (pathname.slice(-8) !== '.svg.min')
+      return false
 
-Site.prototype.serve_fs = async function(env, pathname) {
-  // see serve_internal()
-  // since the file may be huge we need to cache it for as long as reasonable
-  if (this.options.caching)
-    env.response.setHeader('Cache-Control', 'max-age=86400')
-  env.response.setHeader('Content-Type', env.mime_type)
-
-  // see https://dev.to/abdisalan_js/how-to-code-a-video-streaming-server-using-nodejs-2o0
-  let stream
-  let range = env.request.headers.range;
-  if (range !== undefined) {
-    let stats
+    let data
     try {
-      stats = await fsPromises.stat(pathname)
+      data = await this.min_svg_cache.get(pathname)
     }
     catch (err) {
       if (!(err instanceof Error) || err.code !== 'ENOENT')
         throw err
       return false
     }
+    this.serve(env, 200, data, 'min_svg')
+    return true
+  }
 
-    // Parse Range
-    // Example: "bytes=32324-"
-    let start = Number(range.replace(/\D/g, ''))
-    let end = Math.min(start + 1048576, stats.size)
-
-    // see serve()
-    console.log(
-      `${env.parsed_url.host} streaming ${env.parsed_url.pathname} partial ${start}-${end}/${stats.size}`
-    )
-
-    // Create headers
-    env.response.statusCode = 206 // partial content
-    env.response.setHeader(
-      'Content-Range',
-      `bytes ${start}-${end - 1}/${stats.size}`
+  async serve_sass_css(env, pathname) {
+    if (
+      pathname.slice(-9) !== '.css.sass' &&
+        pathname.slice(-9) !== '.css.scss'
     )
-    env.response.setHeader('Accept-Ranges', 'bytes')
-    env.response.setHeader('Content-Length', end - start)
+      return false
 
-    // create video read stream for this particular chunk
-    stream = fs.createReadStream(pathname, {start: start, end: end - 1})
+    let data
+    try {
+      data = await this.sass_css_cache.get(pathname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      return false
+    }
+    this.serve(env, 200, data, 'sass_css')
+    return true
   }
-  else {
-    // see serve()
-    console.log(
-      `${env.parsed_url.host} streaming ${env.parsed_url.pathname}`
-    )
 
+  async serve_fs(env, pathname) {
     // see serve_internal()
-    env.response.statusCode = 200
-    stream = fs.createReadStream(pathname)
-  }
-
-  return /*await*/ new Promise(
-    (resolve, reject) => {
-      stream.on(
-        'error',
-        err => {
-          //console.log(`error: ${err.message}`)
-          if (!(err instanceof Error) || err.code !== 'ENOENT')
-            reject(err)
-          resolve(false)
-        }
+    // since the file may be huge we need to cache it for as long as reasonable
+    if (this.options.caching)
+      env.response.setHeader('Cache-Control', 'max-age=86400')
+    env.response.setHeader('Content-Type', env.mime_type)
+
+    // see https://dev.to/abdisalan_js/how-to-code-a-video-streaming-server-using-nodejs-2o0
+    let stream
+    let range = env.request.headers.range;
+    if (range !== undefined) {
+      let stats
+      try {
+        stats = await fsPromises.stat(pathname)
+      }
+      catch (err) {
+        if (!(err instanceof Error) || err.code !== 'ENOENT')
+          throw err
+        return false
+      }
+
+      // Parse Range
+      // Example: "bytes=32324-"
+      let start = Number(range.replace(/\D/g, ''))
+      let end = Math.min(start + 1048576, stats.size)
+
+      // see serve()
+      console.log(
+        `${env.parsed_url.host} streaming ${env.parsed_url.pathname} partial ${start}-${end}/${stats.size}`
       )
-      stream.on(
-        'data',
-        data => {
-          //console.log(`data: ${data.length} bytes`)
-          env.response.write(data)
-        }
+
+      // Create headers
+      env.response.statusCode = 206 // partial content
+      env.response.setHeader(
+        'Content-Range',
+        `bytes ${start}-${end - 1}/${stats.size}`
       )
-      stream.on(
-        'end',
-        () => {
-          //console.log('end')
-          env.response.end()
-          resolve(true)
-        }
+      env.response.setHeader('Accept-Ranges', 'bytes')
+      env.response.setHeader('Content-Length', end - start)
+
+      // create video read stream for this particular chunk
+      stream = fs.createReadStream(pathname, {start: start, end: end - 1})
+    }
+    else {
+      // see serve()
+      console.log(
+        `${env.parsed_url.host} streaming ${env.parsed_url.pathname}`
       )
+
+      // see serve_internal()
+      env.response.statusCode = 200
+      stream = fs.createReadStream(pathname)
     }
-  )
-}
 
-Site.prototype.serve_zip = async function(env, zipname, pathname) {
-  let zip
-  try {
-    zip = await this.zip_cache.get(zipname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    return false
+    return /*await*/ new Promise(
+      (resolve, reject) => {
+        stream.on(
+          'error',
+          err => {
+            //console.log(`error: ${err.message}`)
+            if (!(err instanceof Error) || err.code !== 'ENOENT')
+              reject(err)
+            resolve(false)
+          }
+        )
+        stream.on(
+          'data',
+          data => {
+            //console.log(`data: ${data.length} bytes`)
+            env.response.write(data)
+          }
+        )
+        stream.on(
+          'end',
+          () => {
+            //console.log('end')
+            env.response.end()
+            resolve(true)
+          }
+        )
+      }
+    )
   }
-  if (!Object.prototype.hasOwnProperty.call(zip, pathname))
-    return false
-  this.serve(env, 200, zip[pathname], 'zip')
-  return true
-}
 
-Site.prototype.serve_file = async function(env, pathname) {
-  //console.log(`serve_file ${pathname}`)
-  if (await this.serve_jst(env, pathname + '.jst'))
-    return
-  if (await this.serve_less_css(env, pathname + '.less'))
-    return
-  if (await this.serve_min_css(env, pathname + '.min'))
-    return
-  if (await this.serve_min_html(env, pathname + '.min'))
-    return
-  if (await this.serve_min_js(env, pathname + '.min'))
-    return
-  if (await this.serve_min_svg(env, pathname + '.min'))
-    return
-  if (await this.serve_sass_css(env, pathname + '.sass'))
-    return
-  if (await this.serve_sass_css(env, pathname + '.scss'))
-    return
-  if (await this.serve_fs(env, pathname))
-    return
-  this.die(env, `file not found ${env.parsed_url.pathname}`)
-}
+  async serve_zip(env, zipname, pathname) {
+    let zip
+    try {
+      zip = await this.zip_cache.get(zipname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      return false
+    }
+    if (!Object.prototype.hasOwnProperty.call(zip, pathname))
+      return false
+    this.serve(env, 200, zip[pathname], 'zip')
+    return true
+  }
 
-Site.prototype.serve_dir = async function(env, pathname, components) {
-  if (await this.serve_jst(env, pathname + '.dir.jst', pathname, components))
-    return
-
-  let stats
-  try {
-    stats = await fsPromises.stat(pathname)
-  }
-  catch (err) {
-    if (!(err instanceof Error) || err.code !== 'ENOENT')
-      throw err
-    this.die(env, `directory not found ${pathname}`)
-    return
-  }
-  if (!stats.isDirectory()) {
-    this.die(
-      env,
-      components.length > 1 ?
-        `not directory ${pathname}` :
-        `unknown file type in ${env.parsed_url.pathname}`
-    )
-    return
+  async serve_file(env, pathname) {
+    //console.log(`serve_file ${pathname}`)
+    if (await this.serve_jst(env, pathname + '.jst'))
+      return
+    if (await this.serve_less_css(env, pathname + '.less'))
+      return
+    if (await this.serve_min_css(env, pathname + '.min'))
+      return
+    if (await this.serve_min_html(env, pathname + '.min'))
+      return
+    if (await this.serve_min_js(env, pathname + '.min'))
+      return
+    if (await this.serve_min_svg(env, pathname + '.min'))
+      return
+    if (await this.serve_sass_css(env, pathname + '.sass'))
+      return
+    if (await this.serve_sass_css(env, pathname + '.scss'))
+      return
+    if (await this.serve_fs(env, pathname))
+      return
+    this.die(env, `file not found ${env.parsed_url.pathname}`)
   }
-  return /*await*/ this.serve_path(env, pathname, components)
-}
 
-Site.prototype.serve_path = async function(env, pathname, components) {
-  //console.log(`serve_path ${pathname} ${components}`)
-  if (components.length === 0) {
-    // directory without trailing slash
-    this.redirect(env, env.parsed_url.pathname + '/index.html')
-    return
-  }
-
-  if (components[0].length === 0) {
-    if (components.length > 1)
-      this.die(env, `empty directory name in ${env.parsed_url.pathname}`)
-    else
-      // directory with trailing slash
-      this.redirect(env, env.parsed_url.pathname + 'index.html')
-    return
-  }
-
-  if (
-    components[0].charAt(0) === '.' ||
-    components[0].charAt(0) === '_'
-  ) {
-    this.die(env, `bad component "${components[0]}" in ${env.parsed_url.pathname}`)
-    return
-  }
-
-  let i = components[0].lastIndexOf('.')
-  if (i === -1)
-    i = components[0].length
-  let extension = components[0].slice(i)
-
-  pathname = `${pathname}/${components[0]}`
-  if (
-    extension.length !== 0 &&
-    Object.prototype.hasOwnProperty.call(this.options.mime_types, extension)
-  ) {
-    if (components.length > 1) {
-      this.die(env, `non-directory extension "${extension}" in ${env.parsed_url.pathname}`)
+  async serve_dir(env, pathname, components) {
+    if (await this.serve_jst(env, pathname + '.dir.jst', pathname, components))
+      return
+
+    let stats
+    try {
+      stats = await fsPromises.stat(pathname)
+    }
+    catch (err) {
+      if (!(err instanceof Error) || err.code !== 'ENOENT')
+        throw err
+      this.die(env, `directory not found ${pathname}`)
+      return
+    }
+    if (!stats.isDirectory()) {
+      this.die(
+        env,
+        components.length > 1 ?
+          `not directory ${pathname}` :
+          `unknown file type in ${env.parsed_url.pathname}`
+      )
       return
     }
-    return /*await*/ this.serve_file(env, pathname)
+    return /*await*/ this.serve_path(env, pathname, components)
   }
-  return /*await*/ this.serve_dir(env, pathname, components.slice(1))
-}
 
-Site.prototype.respond = async function(env) {
-  env.mime_type = 'application/octet-stream'
-  env.caching = this.options.caching
-  let pathname = decodeURIComponent(env.parsed_url.pathname)
-  let i = pathname.lastIndexOf('.')
-  if (i !== -1) {
-    let extension = pathname.slice(i)
+  async serve_path(env, pathname, components) {
+    //console.log(`serve_path ${pathname} ${components}`)
+    if (components.length === 0) {
+      // directory without trailing slash
+      this.redirect(env, env.parsed_url.pathname + '/index.html')
+      return
+    }
+
+    if (components[0].length === 0) {
+      if (components.length > 1)
+        this.die(env, `empty directory name in ${env.parsed_url.pathname}`)
+      else
+        // directory with trailing slash
+        this.redirect(env, env.parsed_url.pathname + 'index.html')
+      return
+    }
+
     if (
+      components[0].charAt(0) === '.' ||
+      components[0].charAt(0) === '_'
+    ) {
+      this.die(env, `bad component "${components[0]}" in ${env.parsed_url.pathname}`)
+      return
+    }
+
+    let i = components[0].lastIndexOf('.')
+    if (i === -1)
+      i = components[0].length
+    let extension = components[0].slice(i)
+
+    pathname = `${pathname}/${components[0]}`
+    if (
+      extension.length !== 0 &&
       Object.prototype.hasOwnProperty.call(this.options.mime_types, extension)
-    )
-      env.mime_type = this.options.mime_types[extension]
+    ) {
+      if (components.length > 1) {
+        this.die(env, `non-directory extension "${extension}" in ${env.parsed_url.pathname}`)
+        return
+      }
+      return /*await*/ this.serve_file(env, pathname)
+    }
+    return /*await*/ this.serve_dir(env, pathname, components.slice(1))
   }
-  if (
-    await this.serve_zip(
-      env,
-      this.root + '/_favicon/favicons.zip',
-      pathname
+
+  async respond(env) {
+    env.mime_type = 'application/octet-stream'
+    env.caching = this.options.caching
+    let pathname = decodeURIComponent(env.parsed_url.pathname)
+    let i = pathname.lastIndexOf('.')
+    if (i !== -1) {
+      let extension = pathname.slice(i)
+      if (
+        Object.prototype.hasOwnProperty.call(this.options.mime_types, extension)
+      )
+        env.mime_type = this.options.mime_types[extension]
+    }
+    if (
+      await this.serve_zip(
+        env,
+        this.root + '/_favicon/favicons.zip',
+        pathname
+      )
     )
-  )
-    return
-  let components = pathname.split('/')
-  if (components.length) {
-    assert(components[0].length == 0)
-    components = components.slice(1)
-  }
-
-  // deal with ACME challenges for certbot (letsencrypt)
-  if (components[0] === '.well-known') {
-    // build path, ensuring that remaining components are safe
-    /*let*/ pathname = `${this.options.certbot_webroot}/.well-known`
-    for (let i = 1; i < components.length; ++i) {
-      if (components[i].charAt(0) == '.') {
-        this.die(env, `bad component "${components[i]}" in ${env.parsed_url.pathname}`)
-        return
+      return
+    let components = pathname.split('/')
+    if (components.length) {
+      assert(components[0].length == 0)
+      components = components.slice(1)
+    }
+
+    // deal with ACME challenges for certbot (letsencrypt)
+    if (components[0] === '.well-known') {
+      // build path, ensuring that remaining components are safe
+      /*let*/ pathname = `${this.options.certbot_webroot}/.well-known`
+      for (let i = 1; i < components.length; ++i) {
+        if (components[i].charAt(0) == '.') {
+          this.die(env, `bad component "${components[i]}" in ${env.parsed_url.pathname}`)
+          return
+        }
+        pathname = `${pathname}/${components[i]}`
       }
-      pathname = `${pathname}/${components[i]}`
+
+      // use serve_fs() because challenge files have no extension
+      return /*await*/ this.serve_fs(env, pathname)
     }
 
-    // use serve_fs() because challenge files have no extension
-    return /*await*/ this.serve_fs(env, pathname)
+    return /*await*/ this.serve_path(env, this.root, components)
   }
-
-  return /*await*/ this.serve_path(env, this.root, components)
 }
 
 export default Site
diff --git a/cli.mjs b/cli.mjs
index 2577a19..37aa6b9 100755 (executable)
--- a/cli.mjs
+++ b/cli.mjs
@@ -1,7 +1,7 @@
 #!/usr/bin/env node
 
 /*
- * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * Copyright (C) 2018-2022 Nick Downing <nick@ndcode.org>
  * SPDX-License-Identifier: MIT
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy