Implement overrideable function bc.build() instead of the callback build_func(),...
authorNick Downing <nick@ndcode.org>
Sat, 1 Dec 2018 23:59:44 +0000 (10:59 +1100)
committerNick Downing <nick@ndcode.org>
Sun, 2 Dec 2018 00:06:13 +0000 (11:06 +1100)
BuildCache.js
README.md

index b48702f..0c7520f 100644 (file)
@@ -28,18 +28,22 @@ let fs_stat = util.promisify(fs.stat)
 
 let BuildCache = function(diag) {
   if (!this instanceof BuildCache)
-    throw Error('BuildCache is a constructor')
+    throw new Error('BuildCache is a constructor')
   this.map = new Map()
   this.diag = diag || false
 }
 
-BuildCache.prototype.get = async function(key, build_func) {
+BuildCache.prototype.build = async function(key, result) {
+  throw new Error('not implemented')
+}
+
+BuildCache.prototype.get = async function(key, once) {
   let result = this.map.get(key)
   if (result === undefined) {
     if (this.diag)
-      console.log('building', key)
+      console.log(`building ${key}`)
     result = {deps: [key], time: Date.now()}
-    result.done = build_func(result)
+    result.done = this.build(key, result)
     this.map.set(key, result)
     try {
       await result.done
@@ -53,7 +57,7 @@ BuildCache.prototype.get = async function(key, build_func) {
   }
   else if (result.done === undefined) {
     if (this.diag)
-      console.log('checking', key)
+      console.log(`checking ${key}`)
     result.done = (
       async () => {
         for (let i = 0; i < result.deps.length; ++i) {
@@ -68,10 +72,10 @@ BuildCache.prototype.get = async function(key, build_func) {
           }
           if (stats === undefined || stats.mtimeMs > result.time) {
             if (this.diag)
-              console.log('rebuilding', key, 'reason', result.deps[i])
+              console.log(`rebuilding ${key} reason ${result.deps[i]}`)
             result.deps = [key]
             result.time = Date.now()
-            await build_func(result)
+            await this.build(key, result)
             break
           }
         }
@@ -89,7 +93,10 @@ BuildCache.prototype.get = async function(key, build_func) {
   }
   else
     await result.done
-  return result.value
+  let value = result.value
+  if (once)
+    result.value = undefined
+  return value
 }
 
 module.exports = BuildCache
index ad90319..82d285c 100644 (file)
--- a/README.md
+++ b/README.md
@@ -18,16 +18,23 @@ objects are added to the cache by attempting to `get` them.
 
 The interface for the `BuildCache`-provided instance function `bc.get()` is:
 
-`await bc.get(key, build_func)` &mdash; retrieves the object stored under `key`,
-the `key` is a somewhat arbitrary string that usually corresponds to the on-disk
+`await bc.get(key, once)` &mdash; retrieves the object stored under `key`, the
+`key` is a somewhat arbitrary string that usually corresponds to the on-disk
 path to the main source file of the wanted object. If `key` already exists in
 the cache, then the corresponding object is returned after an up-to-date check.
-Otherwise, the user-provided `build_func` is called, to build it from sources.
+Otherwise, the user-provided `bc.build()` is called, to build it from sources.
 
-The interface for the user-provided callback function `build_func()` is:
+If the `once` argument is specified `true`, then the cached value is atomically
+set to `undefined` when the built object is returned. This is useful if the
+built object is something like a constructor function, that only needs to be
+called once each time it changes on-disk. Then, the cache does not store the
+build result, but only the list of sources and the time last built (in order to
+detect changes). Omit this argument in the case that normal caching is needed.
 
-`await build_func(result)` &mdash; user must set `result.value` to the object to
-be stored in the cache, and _may_ set `result.deps` to a list of pathnames
+The function `bc.build()` must be overridden by the user in a derived class:
+
+`await bc.build(key, result)` &mdash; user must set `result.value` to an object
+to be stored in the cache, and _may_ set `result.deps` to a list of pathnames
 corresponding to the build dependencies, including the main file which is
 normally the same as the `key` argument to `bc.get()`. The `result.deps` field
 is set to `[key]` before the callback and doesn't need to be changed in the
@@ -44,50 +51,44 @@ dependencies. They are placed in `result.value` and `result.deps` respectively.
 At the moment we do not support _optional_ source files, for instance there
 might be a picture `image.jpg` and an optional metadata file `image.json` that
 goes with it. To handle this case, in the future we could add a _not exists_
-dependency type so that a rebuild occurs if the metadata file appears later on.
+dependency type so that a rebuild occurs if the metadata file becomes present.
 
 ## Usage example
 
-Here is a fairly self-contained example of how we can define a function called
-`get_less(dirname, pathname)` which will load a `Less` stylesheet from the
-given pathname, and compile it to CSS via the cache. The `dirname` is given so
-that if the `Less` stylesheet includes further stylesheets they will be taken
-from the correct path, usually the path containing the main `Less` stylesheet.
+Here is a fairly self-contained example of how we can define a `BuildCacheLess`
+object constructed by `new BuildCacheLess(root)`, whose `get()` function would
+load a `Less` stylesheet from the given pathname, and compile it to CSS via the
+cache. The `root` is used if the `Less` stylesheet includes further stylesheets
+using absolute pathnames, often it would be the document root of the webserver.
 ```js
 let BuildCache = require('@ndcode/build_cache')
 let fs = require('fs')
 let less = require('less/lib/less-node')
+let path = require('path')
 let util = require('util')
 
 let fs_readFile = util.promisify(fs.readFile)
 
-let build_cache_less = new BuildCache()
-let get_less = (dirname, pathname) => {
-  pathname = dirname + pathname
-  return /*await*/ build_cache_less.get(
-    pathname,
-    async result => {
-      let text = await fs_readFile(pathname, {encoding: 'utf-8'})
-      console.log('getting', pathname, 'as less')
-      let render = await less.render(
-        text,
-        {
-          filename: pathname,
-          pathnames: [dirname],
-          rootpathname: this.root
-        }
-      )
-      result.deps.concat(render.imports)
-      result.value = Buffer.from(render.css)
+let BuildCacheLess = function(root) {
+  BuildCache.prototype.call(this)
+  this.root = root
+}
+
+BuildCacheLess.prototype = Object.create(BuildCache.prototype)
+
+BuildCacheLess.prototype.build = async function(key, result) {
+  let render = await less.render(
+    await fs_readFile(key, {encoding: 'utf-8'}),
+    {
+      filename: key,
+      pathnames: [path.dirname(key)],
+      rootpathname: this.root
     }
   )
+  result.deps = result.deps.concat(render.imports)
+  result.value = Buffer.from(render.css)
 }
 ```
-
-The statement `pathname = dirname + pathname` is simplified for the example, it
-should properly be something like `pathname = path.resolve(dirname, pathname)`
-which would be more likely to create a unique key for the stylesheet, since it would resolve out components like `.` or `..` to give a canonical pathname.
-
 We are relying on `fs_readFile()` or `less.render()` to throw exceptions if the
 original stylesheet or any included stylesheet is not found or contains errors.
 
@@ -102,19 +103,9 @@ level routine, an explicit `await` would be consistent here but less efficient.
 
 ## About repeatable builds
 
-Note that the user has to provide a consistent `build_func` every time the cache
-is accessed. The `build_func` may access variables from the surrounding
-environment, most notably the `key` value since this is not passed into the
-`build_func` from the `bc.get()` call, but this should only be done in a safe
-way, such as accessing rarely-changed configuration information, since if the
-building process is impacted by the environment, there is no point caching it.
-
 If different types of objects need to be cached, use several instances of
 `BuildCache` so that differently-built objects do not conflict with each other.
-We might in future change the API so that the `build_func` is provided at
-construction time rather than access time. Although this might be inconvenient
-from the viewpoint of the callback not being able to access variables from the
-surrounding context of the `bc.get()` call, it would ensure repeatable builds.
+This also applies if the build-process is parameterized, e.g. the `root` above.
 
 ## About asynchronicity
 
@@ -123,9 +114,9 @@ does an `fs.stat()` on each source file prior to returning an object from the
 cache. This means that `bc.get()` is fundamentally an asynchronous operation and
 therefore returns a `Promise`, which we showed as `await bc.get()` above.
 
-Also, the building process may be asynchronous, and so `build_func()` is also
+Also, the building process may be asynchronous, and so `bc.build()` is also
 expected to return a `Promise`. Obviously, `bc.get()` must wait for the
-`build_func()` promise to resolve, indicating that the wanted object is safely
+`bc.build()` promise to resolve, indicating that the wanted object is safely
 stored in the cache, so that it can resolve the `bc.get()` promise with the
 `result.value` that is now associated with the key and wanted by the caller.
 
@@ -150,8 +141,8 @@ rejects, the rejection value (exception object) should be treated as read-only.
 
 ## About deletions
 
-Another corner case happens if source files have been deleted since building, we
-handle this the same as an updated source file and attempt to rebuild it.
+Another corner case happens if source files have been deleted since building,
+we handle this the same as an updated source file and attempt to rebuild it.
 
 Note that deleting the source files does not remove an object from the cache,
 since the deleted source files will only be noticed when the object is accessed