boma 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/async.js CHANGED
@@ -1,14 +1,30 @@
1
1
  import { readFile, writeFile } from 'fs/promises';
2
+ import { resolve } from 'path';
2
3
  import { getDate, getIssueMessage, getSerializationIssues, isErrorWithCode, isPlainMergeableObject, isSyntaxError, logSerializationIssues, sanitizeNonSerializable, } from './common.js';
4
+ const addToJSONQueues = new Map();
5
+ const runAddToJSONExclusive = async (filePath, operation) => {
6
+ const key = resolve(filePath);
7
+ const previous = addToJSONQueues.get(key) ?? Promise.resolve();
8
+ const current = previous.catch(() => undefined).then(operation);
9
+ const settled = current.then(() => undefined, () => undefined);
10
+ addToJSONQueues.set(key, settled);
11
+ try {
12
+ return await current;
13
+ }
14
+ finally {
15
+ if (addToJSONQueues.get(key) === settled) {
16
+ addToJSONQueues.delete(key);
17
+ }
18
+ }
19
+ };
3
20
  export const saveJSONAsync = async (saveInput) => {
4
21
  const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
5
22
  const issues = getSerializationIssues(objToSave);
6
23
  if (issues.length > 0) {
7
24
  !silent && logSerializationIssues(issues);
8
- if (!replaceNonSerializable && !silent) {
25
+ if (!replaceNonSerializable) {
9
26
  const firstIssue = issues[0];
10
27
  const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
11
- console.error(errorMessage);
12
28
  throw new Error(errorMessage);
13
29
  }
14
30
  }
@@ -28,7 +44,7 @@ export const saveJSONAsync = async (saveInput) => {
28
44
  }
29
45
  };
30
46
  export const readJSONAsync = async (props) => {
31
- const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
47
+ const { filePath, createIfNotFound = false, parseJSON = true, silent = true, throwError = false, } = props;
32
48
  try {
33
49
  const savedfile = await readFile(filePath, 'utf8');
34
50
  if (parseJSON) {
@@ -37,41 +53,54 @@ export const readJSONAsync = async (props) => {
37
53
  return savedfile;
38
54
  }
39
55
  catch (err) {
56
+ if (throwError)
57
+ throw err;
40
58
  if (isErrorWithCode(err)) {
41
59
  switch (err.code) {
42
60
  case 'ENOENT':
43
61
  if (createIfNotFound) {
44
62
  !silent && console.log('Try to create: ', filePath);
63
+ const initialValue = typeof createIfNotFound === 'boolean' ? {} : createIfNotFound;
64
+ const initialContent = JSON.stringify(initialValue);
45
65
  try {
46
- const initialContent = typeof createIfNotFound === 'boolean' ? '{}' : JSON.stringify(createIfNotFound);
47
- await writeFile(filePath, initialContent, 'utf8');
66
+ await writeFile(filePath, initialContent, { encoding: 'utf8', flag: 'wx' });
48
67
  }
49
68
  catch (writeErr) {
50
- console.error(`Error creating file ${filePath}: `, writeErr, '\n');
69
+ if (isErrorWithCode(writeErr) && writeErr.code === 'EEXIST') {
70
+ return readJSONAsync({ ...props, createIfNotFound: false });
71
+ }
72
+ if (throwError)
73
+ throw writeErr;
74
+ console.error(`[${getDate()}] Boma got error while creating file ${filePath}: `, writeErr, '\n');
51
75
  }
52
- return (typeof createIfNotFound === 'boolean' ? {} : createIfNotFound);
76
+ return (parseJSON ? initialValue : initialContent);
53
77
  }
54
- console.info("Boma can't find file: ", filePath);
78
+ !silent && console.info("Boma can't find file: ", filePath);
55
79
  return parseJSON ? {} : null;
56
80
  case 'EACCES':
57
- console.error(`Access denied for ${filePath}`);
81
+ !silent && console.error(`[${getDate()}] Access denied error for ${filePath}`);
58
82
  return null;
59
83
  default:
60
- console.error(`Some filesystem error when try readJSON: ${filePath}`);
84
+ !silent && console.error(`[${getDate()}] Boma get some filesystem error when try readJSON: ${filePath}`, err);
61
85
  return null;
62
86
  }
63
87
  }
64
88
  if (isSyntaxError(err)) {
65
- console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
89
+ !silent && console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
66
90
  return null;
67
91
  }
68
- console.error('Function readJSON error:', err, '\n');
92
+ !silent && console.error('Function readJSON error:', err, '\n');
69
93
  return null;
70
94
  }
71
95
  };
72
- export const addToJSONAsync = async (saveInput) => {
73
- const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
74
- const oldJSON = await readJSONAsync({ filePath, createIfNotFound: true, silent });
96
+ export const addToJSONAsync = (saveInput) => runAddToJSONExclusive(saveInput.filePath, async () => {
97
+ const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, throwError = false, } = saveInput;
98
+ const oldJSON = await readJSONAsync({
99
+ filePath,
100
+ createIfNotFound: true,
101
+ silent,
102
+ throwError,
103
+ });
75
104
  if (oldJSON === null || typeof oldJSON !== 'object') {
76
105
  return saveJSONAsync({
77
106
  filePath,
@@ -79,7 +108,7 @@ export const addToJSONAsync = async (saveInput) => {
79
108
  format,
80
109
  logSaving,
81
110
  replaceNonSerializable,
82
- silent
111
+ silent,
83
112
  });
84
113
  }
85
114
  if (Array.isArray(oldJSON) && Array.isArray(dataToAdd)) {
@@ -89,7 +118,7 @@ export const addToJSONAsync = async (saveInput) => {
89
118
  format,
90
119
  logSaving,
91
120
  replaceNonSerializable,
92
- silent
121
+ silent,
93
122
  });
94
123
  return;
95
124
  }
@@ -100,20 +129,22 @@ export const addToJSONAsync = async (saveInput) => {
100
129
  format,
101
130
  logSaving,
102
131
  replaceNonSerializable,
103
- silent
132
+ silent,
104
133
  });
105
134
  return;
106
135
  }
107
- if (Array.isArray(dataToAdd) && isPlainMergeableObject(oldJSON) && Object.keys(oldJSON).length === 0) {
136
+ if (Array.isArray(dataToAdd)
137
+ && isPlainMergeableObject(oldJSON)
138
+ && Object.keys(oldJSON).length === 0) {
108
139
  await saveJSONAsync({
109
140
  filePath,
110
141
  objToSave: [...dataToAdd],
111
142
  format,
112
143
  logSaving,
113
144
  replaceNonSerializable,
114
- silent
145
+ silent,
115
146
  });
116
147
  return;
117
148
  }
118
149
  throw new Error('Cannot merge array with object');
119
- };
150
+ });
package/dist/boma.js CHANGED
@@ -3,14 +3,13 @@ import { addToJSONAsync, readJSONAsync, saveJSONAsync } from './async.js';
3
3
  import { getDate, getIssueMessage, getSerializationIssues, isErrorWithCode, isPlainMergeableObject, isSyntaxError, logSerializationIssues, sanitizeNonSerializable } from './common.js';
4
4
  export * from './common.js';
5
5
  const saveJSONSync = (saveInput) => {
6
- const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
6
+ const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true } = saveInput;
7
7
  const issues = getSerializationIssues(objToSave);
8
8
  if (issues.length > 0) {
9
9
  !silent && logSerializationIssues(issues);
10
- if (!replaceNonSerializable && !silent) {
10
+ if (!replaceNonSerializable) {
11
11
  const firstIssue = issues[0];
12
12
  const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
13
- console.error(errorMessage);
14
13
  throw new Error(errorMessage);
15
14
  }
16
15
  }
@@ -30,7 +29,7 @@ const saveJSONSync = (saveInput) => {
30
29
  }
31
30
  };
32
31
  const readJSONSync = (props) => {
33
- const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
32
+ const { filePath, createIfNotFound = false, parseJSON = true, silent = true, throwError = false, } = props;
34
33
  try {
35
34
  const savedfile = readFileSync(filePath, 'utf8');
36
35
  if (parseJSON) {
@@ -39,41 +38,49 @@ const readJSONSync = (props) => {
39
38
  return savedfile;
40
39
  }
41
40
  catch (err) {
41
+ if (throwError)
42
+ throw err;
42
43
  if (isErrorWithCode(err)) {
43
44
  switch (err.code) {
44
45
  case 'ENOENT':
45
46
  if (createIfNotFound) {
46
47
  !silent && console.log('Try to create: ', filePath);
48
+ const initialValue = typeof createIfNotFound === 'boolean' ? {} : createIfNotFound;
49
+ const initialContent = JSON.stringify(initialValue);
47
50
  try {
48
- const initialContent = typeof createIfNotFound === 'boolean' ? '{}' : JSON.stringify(createIfNotFound);
49
- writeFileSync(filePath, initialContent, 'utf8');
51
+ writeFileSync(filePath, initialContent, { encoding: 'utf8', flag: 'wx' });
50
52
  }
51
53
  catch (writeErr) {
52
- console.error(`Error creating file ${filePath}: `, writeErr, '\n');
54
+ if (isErrorWithCode(writeErr) && writeErr.code === 'EEXIST') {
55
+ return readJSONSync({ ...props, createIfNotFound: false });
56
+ }
57
+ if (throwError)
58
+ throw writeErr;
59
+ console.error(`[${getDate()}] Boma got error while creating file ${filePath}: `, writeErr, '\n');
53
60
  }
54
- return (typeof createIfNotFound === 'boolean' ? {} : createIfNotFound);
61
+ return (parseJSON ? initialValue : initialContent);
55
62
  }
56
- console.info("Boma can't find file: ", filePath);
63
+ !silent && console.info("Boma can't find file: ", filePath);
57
64
  return parseJSON ? {} : null;
58
65
  case 'EACCES':
59
- console.error(`Access denied for ${filePath}`);
66
+ !silent && console.error(`Access denied for ${filePath}`);
60
67
  return null;
61
68
  default:
62
- console.error(`Some filesystem error when try readJSON: ${filePath}`);
69
+ !silent && console.error(`Some filesystem error when try readJSON: ${filePath}`, err);
63
70
  return null;
64
71
  }
65
72
  }
66
73
  if (isSyntaxError(err)) {
67
- console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
74
+ !silent && console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
68
75
  return null;
69
76
  }
70
- console.error('Function readJSON error:', err, '\n');
77
+ !silent && console.error('Function readJSON error:', err, '\n');
71
78
  return null;
72
79
  }
73
80
  };
74
81
  const addToJSONSync = (saveInput) => {
75
- const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
76
- const oldJSON = readJSONSync({ filePath, createIfNotFound: true, silent });
82
+ const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, throwError = false, } = saveInput;
83
+ const oldJSON = readJSONSync({ filePath, createIfNotFound: true, silent, throwError });
77
84
  if (oldJSON === null || typeof oldJSON !== 'object') {
78
85
  return saveJSONSync({ filePath, objToSave: dataToAdd, format, logSaving, replaceNonSerializable, silent });
79
86
  }
package/dist/common.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface ReadJSONProps {
11
11
  parseJSON?: boolean;
12
12
  createIfNotFound?: boolean | SerializableObject | SerializableArray;
13
13
  silent?: boolean;
14
+ throwError?: boolean;
14
15
  async?: boolean;
15
16
  }
16
17
  export interface SaveJSONProps {
@@ -29,6 +30,7 @@ export interface addToJSONProps {
29
30
  logSaving?: boolean;
30
31
  replaceNonSerializable?: boolean;
31
32
  silent?: boolean;
33
+ throwError?: boolean;
32
34
  async?: boolean;
33
35
  }
34
36
  export type ReadJSONSyncProps = ReadJSONProps & {
package/dist/common.js CHANGED
@@ -168,6 +168,9 @@ export const sanitizeNonSerializable = (value, path = '$', ancestors = new WeakM
168
168
  if (ancestors.has(value)) {
169
169
  return 'circular';
170
170
  }
171
+ if (typeof value.toJSON === 'function') {
172
+ return sanitizeNonSerializable(value.toJSON(), path, ancestors);
173
+ }
171
174
  ancestors.set(value, path);
172
175
  const result = {};
173
176
  for (const key of Object.keys(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boma",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Simple JSON helper",
5
5
  "main": "./dist/boma.js",
6
6
  "types": "./dist/boma.d.ts",
package/readme.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Boma JSON helper
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/boma?color=%20027dec)](https://www.npmjs.org/package/boma)
4
+
3
5
  Super-simple helper for reading, saving and updating JSON files.
4
6
 
5
7
  Works in synchronous mode by default.
@@ -7,6 +9,17 @@ Pass `async: true` to use the asynchronous implementation.
7
9
 
8
10
  Old synchronous calls remain unchanged.
9
11
 
12
+ # Basic features
13
+
14
+ - Using the `createIfNotFound` parameter in `readJSON` and `addToJSONS`, you can immediately create a missing file with the specified (default) object or array without any additional steps. This is convenient, for example, for writing configuration files.
15
+ - `addToJSON` — reads a file and shallow merges objects or concatenates arrays.
16
+ - Flag `replaceNonSerializable` — replaces `function`, `undefined`, `bigint`, `loops`, `NaN`, and `Infinity` with understandable markers.
17
+ - Flag `getSerializationIssues` — shows all problematic fields with paths like `[OBJECT].user.callback`.
18
+ - Single interface with `async: true`, instead of separate `readFile` / `readFileSync`.
19
+ - You can get raw text using flag `parseJSON`: false.
20
+ - By default, boma hides many errors when reading a file and returns `null` or `{}` — this is very convenient in a production environment. If necessary, you can set the `throwError: true` in `readJSON` and `addToJSONS` to catch and handle all errors yourself. However, write errors are much more critical, so `boma` always throws them.
21
+ - Concurrent `addToJSON` calls within the same process are serialized per file, preventing lost updates from overlapping read–merge–write cycles. **Important! This does not provide cross-process or cross-thread locking, so multiple workers or external writers may still cause race conditions.**
22
+
10
23
  # Install
11
24
 
12
25
  ```bash