2 * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
3 * SPDX-License-Identifier: MIT
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to
7 * deal in the Software without restriction, including without limitation the
8 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9 * sell copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24 let fs = require('fs')
25 let util = require('util')
27 let fs_stat = util.promisify(fs.stat)
30 * Constructs a cache object. The cache object is intended to store objects of
31 * arbitrary JavaScript type, which are built from on-disk source files of some
32 * kind. The cache tracks the source files of each object, and makes sure the
33 * objects are rebuilt as required if the source files change on disk.
36 * @param {boolean} diag - Should diagnostic messages be printed to the
39 let BuildCache = function(diag) {
40 if (!this instanceof BuildCache)
41 throw new Error('BuildCache is a constructor')
43 this.diag = diag || false
47 * Abstract method which is expected to build and return an object, given its
48 * key. Called from "get()" when the object does not exist or is out of date.
50 * If this method throws an exception, the key will be deleted from the cache
51 * and the exception re-thrown to the caller of "get()". If there are multiple
52 * callers to "get()" blocking and waiting for the build, they all receive the
53 * same exception object. So one has to be careful the exception is shareable.
56 * @param {string} key - Usually the path to the main source file on disk.
57 * @param {object} result - A dictionary to receive information about the built
58 * object, you can optionally set "result.deps" to a list of dependency files
59 * whose modification would invalidate the just-built and cached object.
61 BuildCache.prototype.build = async function(key, result) {
62 throw new Error('not implemented')
66 * Retrieves the object stored in the cache under "key". If "key" already
67 * exists in the cache, then it will be checked for up-to-dateness. If present
68 * and up-to-date then its object is returned directly. Otherwise the abstract
69 * "build()" method is called to attempt to build the object, and either an
70 * exception is thrown or the built object is stored and returned to the
73 * Other callers requsting the same object while the original build progresses
74 * will be blocked, and all will wait for the build to complete. In this time,
75 * no new up-to-date check will be initiated. But as soon as the build is
76 * completed and the cache updated, further up-to-date checks become possible.
78 * An interesting alternate usage is provided for objects whose contents only
79 * matter if they have been rebuilt since last time. For example, suppose we
80 * want to periodically read a configuration file, and then possibly restart
81 * some long-running process if the configuration has changed. Then it is not
82 * necessary to store the result of configuration parsing in the cache, since
83 * it is only needed momentarily (while we're actually restarting the process).
84 * In such case, pass "once = true" and an "undefined" return means no change.
87 * @param {string} key - Usually the path to the main source file on disk.
88 * @param {boolean} once - If "true", it means the returned object will only be
89 * used once. See above for a more comprehensive discussion of this feature.
91 BuildCache.prototype.get = async function(key, once) {
92 let result = this.map.get(key)
93 if (result === undefined) {
95 console.log(`building ${key}`)
96 result = {deps: [key], time: Date.now()}
97 result.done = this.build(key, result)
98 this.map.set(key, result)
109 else if (result.done === undefined) {
111 console.log(`checking ${key}`)
114 for (let i = 0; i < result.deps.length; ++i) {
117 stats = await fs_stat(result.deps[i])
120 if (!(err instanceof Error) || err.code !== 'ENOENT')
124 if (stats === undefined || stats.mtimeMs > result.time) {
126 console.log(`rebuilding ${key} reason ${result.deps[i]}`)
128 result.time = Date.now()
129 await this.build(key, result)
147 let value = result.value
149 result.value = undefined
154 * Call this periodically to allow the cache to clean itself of stale objects.
156 * The cache cleaning is not yet implemented, but the dummy "kick()" function
157 * is provided so that you can start to put the cleaning infrastructure in your
158 * code already. The constructor arguments might change later for this feature.
162 BuildCache.prototype.kick = function() {
163 // not yet implemented
166 module.exports = BuildCache