Change from pnpm to npm, add ./link.sh shortcut for npm style package linking
[build_cache.git] / README.md
1 # Build Cache system
2
3 An NDCODE project.
4
5 ## Overview
6
7 The `build_cache` package exports a single constructor `BuildCache(diag)` which
8 must be called with the `new` operator. The resulting cache object is intended
9 to store objects of arbitrary JavaScript type, which are generated from source
10 files of some kind. The cache tracks the source files of each object, and makes
11 sure the objects are rebuilt as required if the source files change on disk.
12
13 ## Calling API
14
15 Suppose one has a `BuildCache` instance named `bc`. It behaves somewhat like an
16 ES6 `Map` object, except that it only has the `bc.get()` function, because new
17 objects are added to the cache by attempting to `get` them.
18
19 The interface for the `BuildCache`-provided instance function `bc.get()` is:
20
21 `await bc.get(key, once)` — retrieves the object stored under `key`, the
22 `key` is a somewhat arbitrary string that usually corresponds to the on-disk
23 path to the main source file of the wanted object. If `key` already exists in
24 the cache, then the corresponding object is returned after an up-to-date check.
25 Otherwise, the user-provided `bc.build()` is called, to build it from sources.
26
27 If the `once` argument is specified `true`, then the cached value is atomically
28 set to `undefined` when the built object is returned. This is useful if the
29 built object is something like a constructor function, that only needs to be
30 called once each time it changes on-disk. Then, the cache does not store the
31 build result, but only the list of sources and the time last built (in order to
32 detect changes). Omit this argument in the case that normal caching is needed.
33
34 The function `bc.build()` must be overridden by the user in a derived class:
35
36 `await bc.build(key, result)` — user must set `result.value` to an object
37 to be stored in the cache, and _may_ set `result.deps` to a list of pathnames
38 corresponding to the build dependencies, including the main file which is
39 normally the same as the `key` argument to `bc.get()`. The `result.deps` field
40 is set to `[key]` before the callback and doesn't need to be changed in the
41 common case that exactly one source file compiles to one object in the cache.
42
43 ## About dependencies
44
45 Usually the dependencies will be tracked during the building of an object, for
46 instance the main source file may contain `include` directives that bring in
47 further source files. Most compilers should support this feature, for instance
48 the programmatic API to the `Less` CSS compiler returns CSS code and a list of
49 dependencies. They are placed in `result.value` and `result.deps` respectively.
50
51 At the moment we do not support _optional_ source files, for instance there
52 might be a picture `image.jpg` and an optional metadata file `image.json` that
53 goes with it. To handle this case, in the future we could add a _not exists_
54 dependency type so that a rebuild occurs if the metadata file becomes present.
55
56 ## Usage example
57
58 Here is a fairly self-contained example of how we can define a `BuildCacheLess`
59 object constructed by `new BuildCacheLess(root)`, whose `get()` function would
60 load a `Less` stylesheet from the given pathname, and compile it to CSS via the
61 cache. The `root` is used if the `Less` stylesheet includes further stylesheets
62 using absolute pathnames, often it would be the document root of the webserver.
63 ```js
64 let BuildCache = require('@ndcode/build_cache')
65 let fs = require('fs')
66 let less = require('less/lib/less-node')
67 let path = require('path')
68 let util = require('util')
69
70 let fs_readFile = util.promisify(fs.readFile)
71
72 let BuildCacheLess = function(root) {
73   BuildCache.prototype.call(this)
74   this.root = root
75 }
76
77 BuildCacheLess.prototype = Object.create(BuildCache.prototype)
78
79 BuildCacheLess.prototype.build = async function(key, result) {
80   let render = await less.render(
81     await fs_readFile(key, {encoding: 'utf-8'}),
82     {
83       filename: key,
84       pathnames: [path.dirname(key)],
85       rootpathname: this.root
86     }
87   )
88   result.deps = result.deps.concat(render.imports)
89   result.value = Buffer.from(render.css)
90 }
91 ```
92 We are relying on `fs_readFile()` or `less.render()` to throw exceptions if the
93 original stylesheet or any included stylesheet is not found or contains errors.
94
95 Also, note how much simplified the handling of asynchronicity is when using the
96 ES7 `async`/`await` syntax. We recommend to do this for all new code, and to do
97 it consistently, even if the use of `Promise` directly might give shorter code.
98
99 Code which is not already promisified, can be promisified as shown, or else we
100 can add specific conversions in places by code like: `await new Promise(...)`.
101 Note the comment `/* await */` where a `Promise` is passed through from a lower
102 level routine, an explicit `await` would be consistent here but less efficient.
103
104 ## About repeatable builds
105
106 If different types of objects need to be cached, use several instances of
107 `BuildCache` so that differently-built objects do not conflict with each other.
108 This also applies if the build-process is parameterized, e.g. the `root` above.
109
110 ## About asynchronicity
111
112 To avoid the overhead of _directory watching_, the current implementation just
113 does an `fs.stat()` on each source file prior to returning an object from the
114 cache. This means that `bc.get()` is fundamentally an asynchronous operation and
115 therefore returns a `Promise`, which we showed as `await bc.get()` above.
116
117 Also, the building process may be asynchronous, and so `bc.build()` is also
118 expected to return a `Promise`. Obviously, `bc.get()` must wait for the
119 `bc.build()` promise to resolve, indicating that the wanted object is safely
120 stored in the cache, so that it can resolve the `bc.get()` promise with the
121 `result.value` that is now associated with the key and wanted by the caller.
122
123 There are some rather tricky corner cases associated with this, such as what
124 happens when the same object is requested again while its up-to-dateness is
125 being checked or while it is being built. `BuildCache` correctly handles these
126 cases. Whilst in general the up-to-date check happens every time an object is
127 retrieved, it won't be overlapped with another up-to-date check or a build.
128
129 ## About exceptions
130
131 Exceptions during the build process are handled by reflecting them through both
132 `Promise`s, and also invalidating the associated key on the way through, so that
133 the object is no longer cached, and a fresh rebuild will be attempted should it
134 be accessed again in the future. A build failure is the only way that an object
135 can be removed from the cache (we may add an explicit removal API).
136
137 Note that if several callers are requesting the same key simultaneously and an
138 exception occurs during the build or up-to-date check, each caller receives a
139 reference to same shared exception object, thus when the `bc.get()` `Promise`
140 rejects, the rejection value (exception object) should be treated as read-only.
141
142 ## About deletions
143
144 Another corner case happens if source files have been deleted since building,
145 we handle this the same as an updated source file and attempt to rebuild it.
146
147 Note that deleting the source files does not remove an object from the cache,
148 since the deleted source files will only be noticed when the object is accessed
149 (however, the resulting rebuild will remove the cached object if it fails).
150
151 ## About diagnostics
152
153 The `diag` argument to the constructor is a `bool`, which if `true` causes
154 messages to be printed via `console.log()` for all activities except for the
155 common case of retrieval when the object is already up-to-date. A `diag` value
156 of `undefined` is treated as `false`, thus it can be omitted in the usual case.
157
158 The `diag` output is handy for development, and can also be handy in production,
159 e.g. our production server is started by `systemd` which automatically routes
160 `stdout` output to the system log, and the cache access diagnostic acts somewhat
161 like an HTTP server's `access.log`, albeit up-to-date accesses are not logged.
162
163 We have not attempted to provide comprehensive logging facilities or
164 log-routing, because the simple expedient is to turn off the built-in
165 diagnostics in complex cases and just do your own. In our server we have the
166 built-in diagnostics enabled in some simple cases and disabled in favour of
167 caller-provided logging in others (we use quite a few BuildCache instances,
168 since there are various preprocessors, including `Less` as mentioned above).
169
170 ## To be implemented
171
172 It is intended that we will shortly add a timer function (or possibly just a
173 function that the user should call periodically) to flush built objects from the
174 cache after a stale time, on the assumption that the object might not be
175 accessible or wanted anymore. For example, if the objects are HTML pages, the
176 link structure of the site may have changed to make some pages inaccessible.
177
178 ## GIT repository
179
180 The development version can be cloned, downloaded, or browsed with `gitweb` at:
181 https://git.ndcode.org/public/build_cache.git
182
183 ## License
184
185 All of our NPM packages are MIT licensed, please see LICENSE in the repository.
186
187 ## Contributions
188
189 We would greatly welcome your feedback and contributions. The `build_cache` is
190 under active development (and is part of a larger project that is also under
191 development) and thus the API is considered tentative and subject to change. If
192 this is undesirable, you could possibly pin the version in your `package.json`.
193
194 Contact: Nick Downing <nick@ndcode.org>