Publish version 0.1.1 to NPM
[json_cache_rw.git] / JSONCache.js
1 /*
2  * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
3  * SPDX-License-Identifier: MIT
4  * 
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:
11  * 
12  * The above copyright notice and this permission notice shall be included in
13  * all copies or substantial portions of the Software.
14  * 
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
21  * IN THE SOFTWARE.
22  */
23
24 let fs = require('fs')
25 let util = require('util')
26
27 let fs_readFile = util.promisify(fs.readFile)
28 let fs_rename = util.promisify(fs.rename)
29 let fs_unlink = util.promisify(fs.unlink)
30 let fs_writeFile = util.promisify(fs.writeFile)
31
32 let JSONCache = function(diag) {
33   if (!this instanceof JSONCache)
34     throw Error('JSONCache is a constructor')
35   this.map = new Map()
36   this.diag = diag || false
37 }
38
39 let read = (pathname, default_value, diag) => {
40   if (diag)
41     console.log('reading', pathname)
42   let result = {dirty: false}
43   result.done = (
44     async () => {
45       let text
46       try {
47         text = await fs_readFile(pathname, {encoding: 'utf-8'})
48       }
49       catch (err) {
50         if (!(err instanceof Error) || err.code !== 'ENOENT')
51           throw err;
52         try {
53           await fs_rename(pathname + '.temp', pathname)
54           text = await fs_readFile(pathname, {encoding: 'utf-8'})
55         }
56         catch (err) {
57           if (
58             default_value === undefined ||
59             !(err instanceof Error) ||
60             err.code !== 'ENOENT'
61           )
62             throw err;
63         }
64       }
65       result.value = text === undefined ? default_value : JSON.parse(text)
66     }
67   )()
68   return result
69 }
70
71 JSONCache.prototype.read = async function(key, default_value) {
72   let result = this.map.get(key)
73   if (result === undefined) {
74     result = read(key, default_value, this.diag)
75     this.map.set(key, result)
76     await result.done
77     delete result.done
78   }
79   else
80     while (result.done !== undefined)
81       await result.done
82   return result.value
83 }
84
85 let write = (pathname, result, timeout, diag) => {
86   if (!result.dirty) {
87     result.dirty = true
88     setTimeout(
89       async () => {
90         if (diag)
91           console.log('writing', pathname)
92         result.dirty = false
93         let temp_pathname = pathname + '.temp'
94         let text = JSON.stringify(result.value) + '\n'
95         try {
96           await fs_writeFile(temp_pathname, text, {encoding: 'utf-8'})
97           try {
98             await fs_unlink(pathname)
99           }
100           catch (err) {
101             if (!(err instanceof Error) || err.code !== 'ENOENT')
102               throw err
103           }
104           await fs_rename(temp_pathname, pathname)
105         }
106         catch (err) {
107           console.err(err.stack || err.message)
108         }
109       },
110       timeout || 5000
111     )
112   }
113 }
114
115 JSONCache.prototype.write = async function(key, value, timeout) {
116   let result = this.map.get(key)
117   if (result === undefined) {
118     // we no longer support passing an undefined value to indicate that the
119     // cached item was modified in-place, this is because we will eventually
120     // implement dropping of less recently accessed objects from the cache
121     //assert(value !== undefined)
122     result = {dirty: false, value: value}
123     this.map.set(key, result)
124   }
125   else { //if (value !== undefined) {
126     while (result.done !== undefined)
127       await result.done
128     result.value = value
129   }
130   write(key, result, timeout, this.diag)
131 }
132
133 JSONCache.prototype.modify = async function(
134   key,
135   default_value,
136   modify_func,
137   timeout
138 ) {
139   // duplicate the get() here, we can't await get() directly because we must 
140   // atomically check that result.done === undefined before the modification
141   let result = this.map.get(key)
142   if (result === undefined) {
143     result = read(key, default_value, this.diag)
144     this.map.set(key, result)
145     await result.done
146     delete result.done
147   }
148   else
149     while (result.done !== undefined)
150       await result.done
151   result.done = (
152     async () => {
153       await modify_func(result)
154       write(key, result, timeout, this.diag)
155     }
156   )()
157   await result.done
158   delete result.done
159 }
160
161 module.exports = JSONCache