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_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)
32 let JSONCache = function(diag) {
33 if (!this instanceof JSONCache)
34 throw Error('JSONCache is a constructor')
36 this.diag = diag || false
39 let read = (pathname, default_value, diag) => {
41 console.log('reading', pathname)
42 let result = {dirty: false}
47 text = await fs_readFile(pathname, {encoding: 'utf-8'})
50 if (!(err instanceof Error) || err.code !== 'ENOENT')
53 await fs_rename(pathname + '.temp', pathname)
54 text = await fs_readFile(pathname, {encoding: 'utf-8'})
58 default_value === undefined ||
59 !(err instanceof Error) ||
65 result.value = text === undefined ? default_value : JSON.parse(text)
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)
80 while (result.done !== undefined)
85 let write = (pathname, result, timeout, diag) => {
91 console.log('writing', pathname)
93 let temp_pathname = pathname + '.temp'
94 let text = JSON.stringify(result.value) + '\n'
96 await fs_writeFile(temp_pathname, text, {encoding: 'utf-8'})
98 await fs_unlink(pathname)
101 if (!(err instanceof Error) || err.code !== 'ENOENT')
104 await fs_rename(temp_pathname, pathname)
107 console.err(err.stack || err.message)
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)
125 else { //if (value !== undefined) {
126 while (result.done !== undefined)
130 write(key, result, timeout, this.diag)
133 JSONCache.prototype.modify = async function(
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)
149 while (result.done !== undefined)
153 await modify_func(result)
154 write(key, result, timeout, this.diag)
161 module.exports = JSONCache