Publishable version
authorNick Downing <nick@ndcode.org>
Sat, 10 Nov 2018 10:30:07 +0000 (21:30 +1100)
committerNick Downing <nick@ndcode.org>
Sat, 10 Nov 2018 10:30:19 +0000 (21:30 +1100)
.gitignore
.npmignore [new symlink]
.yarnignore [new symlink]
BuildCache.js
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
package.json

index 1826634..9805cda 100644 (file)
@@ -1 +1,5 @@
-/BuildCache-*.tgz
+/build_cache-*.tgz
+/node_modules
+/package-lock.json
+/yarn.lock
+/yarn-error.log
diff --git a/.npmignore b/.npmignore
new file mode 120000 (symlink)
index 0000000..3e4e48b
--- /dev/null
@@ -0,0 +1 @@
+.gitignore
\ No newline at end of file
diff --git a/.yarnignore b/.yarnignore
new file mode 120000 (symlink)
index 0000000..3e4e48b
--- /dev/null
@@ -0,0 +1 @@
+.gitignore
\ No newline at end of file
index 8253401..fbffa49 100644 (file)
@@ -1,3 +1,26 @@
+/*
+ * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * SPDX-License-Identifier: MIT
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
 let fs = require('fs')
 let util = require('util')
 
@@ -39,7 +62,7 @@ BuildCache.prototype.get = async function(key, build_func) {
             stats = await fs_stat(result.deps[i])
           }
           catch (err) {
-            if (err.code !== 'ENOENT') // err type???
+            if (!(err instanceof Error) || err.code !== 'ENOENT')
               throw err
             //stats = undefined
           }
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..aa360c1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+SPDX-License-Identifier: MIT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..3b70f5e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,203 @@
+# Build Cache system
+
+An NDCODE project.
+
+## Overview
+
+The `build_cache` package exports a single constructor `BuildCache(diag)` which
+must be called with the `new` operator. The resulting cache object is intended
+to store objects of arbitrary JavaScript type, which are generated from source
+files of some kind. The cache tracks the source files of each object, and makes
+sure the objects are rebuilt as required if the source files change on disk.
+
+## Calling API
+
+Suppose one has a `BuildCache` instance named `bc`. It behaves somewhat like an
+ES6 `Map` object, except that it only has the `bc.get()` function, because new
+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
+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.
+
+The interface for the user-provided callback function `build_func()` is:
+
+`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
+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
+common case that exactly one source file compiles to one object in the cache.
+
+## About dependencies
+
+Usually the dependencies will be tracked during the building of an object, for
+instance the main source file may contain `include` directives that bring in
+further source files. Most compilers should support this feature, for instance
+the programmatic API to the `Less` CSS compiler returns CSS code and a list of
+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.
+
+## 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.
+```js
+let BuildCache = require('build_cache')
+let fs = require('fs')
+let less = require('less/lib/less-node')
+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)
+    }
+  )
+}
+```
+
+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.
+
+Also, note how much simplified the handling of asynchronicity is when using the
+ES6 `async`/`await` syntax. We recommend to do this for all new code, and to do
+it consistently, even if the use of `Promise` directly might give shorter code.
+
+Code which is not already promisified, can be promisified as shown, or else we
+can add specific conversions in places by code like: `await new Promise(...)`.
+Note the comment `/* await */` where a `Promise` is passed through from a lower
+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.
+
+## About asynchronicity
+
+To avoid the overhead of _directory watching_, the current implementation just
+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
+expected to return a `Promise`. Obviously, `bc.get()` must wait for the
+`build_func()` 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.
+
+There are some rather tricky corner cases associated with this, such as what
+happens when the same object is requested again while its up-to-dateness is
+being checked or while it is being built. `BuildCache` correctly handles these
+cases. Whilst in general the up-to-date check happens every time an object is
+retrieved, it won't be overlapped with another up-to-date check or a build.
+
+## About exceptions
+
+Exceptions during the build process are handled by reflecting them through both
+`Promise`s, and also invalidating the associated key on the way through, so that
+the object is no longer cached, and a fresh rebuild will be attempted should it
+be accessed again in the future. A build failure is the only way that an object
+can be removed from the cache (we may add an explicit removal API).
+
+Note that if several callers are requesting the same key simultaneously and an
+exception occurs during the build or up-to-date check, each caller receives a
+reference to same shared exception object, thus when the `bc.get()` `Promise`
+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.
+
+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
+(however, the resulting rebuild will remove the cached object if it fails).
+
+## About diagnostics
+
+The `diag` argument to the constructor is a `bool`, which if `true` causes
+messages to be printed via `console.log()` for all activities except for the
+common case of retrieval when the object is already up-to-date. A `diag` value
+of `undefined` is treated as `false`, thus it can be omitted in the usual case.
+
+The `diag` output is handy for development, and can also be handy in production,
+e.g. our production server is started by `systemd` which automatically routes
+`stdout` output to the system log, and the cache access diagnostic acts somewhat
+like an HTTP server's `access.log`, albeit up-to-date accesses are not logged.
+
+We have not attempted to provide comprehensive logging facilities or
+log-routing, because the simple expedient is to turn off the built-in
+diagnostics in complex cases and just do your own. In our server we have the
+built-in diagnostics enabled in some simple cases and disabled in favour of
+caller-provided logging in others (we use quite a few BuildCache instances,
+since there are various preprocessors, including `Less` as mentioned above).
+
+## To be implemented
+
+It is intended that we will shortly add a timer function (or possibly just a
+function that the user should call periodically) to flush built objects from the
+cache after a stale time, on the assumption that the object might not be
+accessible or wanted anymore. For example, if the objects are HTML pages, the
+link structure of the site may have changed to make some pages inaccessible.
+
+## GIT repository
+
+The development version can be cloned, downloaded, or browsed with `gitweb` at:
+https://git.ndcode.org/public/build_cache.git
+
+## License
+
+All of our NPM packages are MIT licensed, please see LICENSE in the repository.
+
+## Contributions
+
+We would greatly welcome your feedback and contributions. The `build_cache` is
+under active development (and is part of a larger project that is also under
+development) and thus the API is considered tentative and subject to change. If
+this is undesirable, you could possibly pin the version in your `package.json`.
+
+Contact: Nick Downing <nick@ndcode.org>
index 9b50ff0..c9d3de9 100644 (file)
@@ -1,14 +1,15 @@
 {
-  "name": "BuildCache",
-  "version": "1.0.0",
+  "name": "build_cache",
+  "version": "0.1.0",
   "description": "Simple caching scheme that checks for modified sources",
+  "keywords": ["build", "cache", "modified", "sources", "compile", "compiler"],
+  "homepage": "https://www.ndcode.org",
+  "bugs": {"email": "nick@ndcode.org"},
   "main": "BuildCache.js",
   "directories": {},
-  "dependencies": {
-    "fs": "^0.0.1-security"
-  },
+  "dependencies": {},
   "devDependencies": {},
   "scripts": {},
-  "author": "Nick Downing",
-  "license": "GPL-3.0"
+  "author": "Nick Downing <nick@ndcode.org>",
+  "license": "MIT"
 }