@arcanejs/react-toolkit 0.14.0 → 0.15.0

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/data.d.mts CHANGED
@@ -45,6 +45,10 @@ type DataFileContext<T> = {
45
45
  */
46
46
  lastUpdatedMillis: number;
47
47
  updateData: DataFileUpdater<T>;
48
+ /**
49
+ * Can be called to reset the data to the default value, and save that to disk.
50
+ */
51
+ resetData: () => void;
48
52
  /**
49
53
  * Can be called to force an attempt to re-save the data to disk
50
54
  */
@@ -99,7 +103,8 @@ type DataFileOperation = 'load' | 'save' | 'usage';
99
103
  declare class ArcaneDataFileError extends Error {
100
104
  readonly operation: DataFileOperation;
101
105
  readonly path: string | null;
102
- constructor(message: string, operation: DataFileOperation, path: string | null, cause?: unknown);
106
+ readonly contents: string | null;
107
+ constructor(message: string, operation: DataFileOperation, path: string | null, contents: string | null, cause?: unknown);
103
108
  }
104
109
  type ErrorListener = (error: ArcaneDataFileError) => void;
105
110
  type UseDataFileCoreProps<T> = WithPathChange & {
@@ -111,6 +116,7 @@ type UseDataFileCoreProps<T> = WithPathChange & {
111
116
  type DataFileCore<T> = {
112
117
  data: DataState<T>;
113
118
  updateData: DataFileUpdater<T>;
119
+ resetData: () => void;
114
120
  saveData: () => void;
115
121
  };
116
122
  /**
package/dist/data.d.ts CHANGED
@@ -45,6 +45,10 @@ type DataFileContext<T> = {
45
45
  */
46
46
  lastUpdatedMillis: number;
47
47
  updateData: DataFileUpdater<T>;
48
+ /**
49
+ * Can be called to reset the data to the default value, and save that to disk.
50
+ */
51
+ resetData: () => void;
48
52
  /**
49
53
  * Can be called to force an attempt to re-save the data to disk
50
54
  */
@@ -99,7 +103,8 @@ type DataFileOperation = 'load' | 'save' | 'usage';
99
103
  declare class ArcaneDataFileError extends Error {
100
104
  readonly operation: DataFileOperation;
101
105
  readonly path: string | null;
102
- constructor(message: string, operation: DataFileOperation, path: string | null, cause?: unknown);
106
+ readonly contents: string | null;
107
+ constructor(message: string, operation: DataFileOperation, path: string | null, contents: string | null, cause?: unknown);
103
108
  }
104
109
  type ErrorListener = (error: ArcaneDataFileError) => void;
105
110
  type UseDataFileCoreProps<T> = WithPathChange & {
@@ -111,6 +116,7 @@ type UseDataFileCoreProps<T> = WithPathChange & {
111
116
  type DataFileCore<T> = {
112
117
  data: DataState<T>;
113
118
  updateData: DataFileUpdater<T>;
119
+ resetData: () => void;
114
120
  saveData: () => void;
115
121
  };
116
122
  /**
package/dist/data.js CHANGED
@@ -4,6 +4,7 @@ var _chunkRT2VSMJLjs = require('./chunk-RT2VSMJL.js');
4
4
 
5
5
  // src/data.tsx
6
6
  var _fs = require('fs');
7
+ var _crypto = require('crypto');
7
8
 
8
9
 
9
10
 
@@ -28,13 +29,27 @@ function useDataFile(dataFile, usage) {
28
29
  return dataFile.useDataFile(usage);
29
30
  }
30
31
  var ArcaneDataFileError = class extends Error {
31
- constructor(message, operation, path, cause) {
32
+ constructor(message, operation, path, contents, cause) {
32
33
  super(message, { cause });
33
34
  this.operation = operation;
34
35
  this.path = path;
36
+ this.contents = contents;
35
37
  this.name = `ArcaneDataFileError(${operation})`;
36
38
  }
37
39
  };
40
+ function stripUtf8Bom(data) {
41
+ return data.charCodeAt(0) === 65279 ? data.slice(1) : data;
42
+ }
43
+ async function writeFileAtomically(path, data) {
44
+ const tempPath = `${path}.${process.pid}.${_crypto.randomUUID.call(void 0, )}.tmp`;
45
+ try {
46
+ await _fs.promises.writeFile(tempPath, data, "utf8");
47
+ await _fs.promises.rename(tempPath, path);
48
+ } catch (error) {
49
+ await _fs.promises.rm(tempPath, { force: true }).catch(() => void 0);
50
+ throw error;
51
+ }
52
+ }
38
53
  function useDataFileCore({
39
54
  schema,
40
55
  defaultValue,
@@ -45,6 +60,7 @@ function useDataFileCore({
45
60
  const log = _chunkRT2VSMJLjs.useLogger.call(void 0, );
46
61
  const state = _react.useRef.call(void 0, {
47
62
  initialized: false,
63
+ saveChain: Promise.resolve(),
48
64
  path: null,
49
65
  data: void 0,
50
66
  previousData: void 0,
@@ -60,6 +76,7 @@ function useDataFileCore({
60
76
  const error = new ArcaneDataFileError(
61
77
  "Cannot change schema or defaultValue after initialization",
62
78
  "usage",
79
+ null,
63
80
  null
64
81
  );
65
82
  _optionalChain([onError, 'optionalCall', _ => _(error)]);
@@ -110,11 +127,15 @@ function useDataFileCore({
110
127
  if (!currentPath || currentData === void 0) {
111
128
  return;
112
129
  }
113
- try {
114
- const json = JSON.stringify(currentData, null, 2);
130
+ const json = JSON.stringify(currentData, null, 2);
131
+ const queuedSave = state.current.saveChain.catch(() => void 0).then(async () => {
115
132
  await _fs.promises.mkdir(_path.dirname.call(void 0, currentPath), { recursive: true });
116
- await _fs.promises.writeFile(currentPath, json, "utf8");
117
- if (state.current.path === currentPath && state.current.data === currentData) {
133
+ await writeFileAtomically(currentPath, json);
134
+ });
135
+ try {
136
+ state.current.saveChain = queuedSave;
137
+ await queuedSave;
138
+ if (state.current.path === currentPath && state.current.saveChain === queuedSave) {
118
139
  state.current.state = { state: "saved" };
119
140
  }
120
141
  } catch (cause) {
@@ -122,10 +143,11 @@ function useDataFileCore({
122
143
  `Error saving data file to path: ${currentPath}`,
123
144
  "save",
124
145
  currentPath,
146
+ null,
125
147
  cause
126
148
  );
127
149
  _optionalChain([onError, 'optionalCall', _2 => _2(error)]);
128
- if (state.current.path === currentPath && state.current.data === currentData) {
150
+ if (state.current.path === currentPath && state.current.saveChain === queuedSave) {
129
151
  state.current.state = { state: "error", error };
130
152
  updateDataFromState();
131
153
  }
@@ -152,8 +174,10 @@ function useDataFileCore({
152
174
  state: "saved"
153
175
  }
154
176
  };
177
+ let contents = null;
155
178
  _fs.promises.readFile(path, "utf8").then((data2) => {
156
- const parsedData = schema.parse(JSON.parse(data2));
179
+ contents = data2;
180
+ const parsedData = schema.parse(JSON.parse(stripUtf8Bom(data2)));
157
181
  if (state.current.path === path) {
158
182
  state.current.data = parsedData;
159
183
  state.current.lastUpdatedMillis = Date.now();
@@ -182,6 +206,7 @@ function useDataFileCore({
182
206
  `Error loading data file at path: ${path}`,
183
207
  "load",
184
208
  path,
209
+ contents,
185
210
  err
186
211
  );
187
212
  _optionalChain([onError, 'optionalCall', _5 => _5(error)]);
@@ -190,25 +215,36 @@ function useDataFileCore({
190
215
  updateDataFromState();
191
216
  });
192
217
  }, [path, onPathChange]);
193
- const updateData = _react.useMemo.call(void 0,
194
- () => (update) => {
218
+ const setDataTo = _react.useMemo.call(void 0,
219
+ () => (data2) => {
195
220
  if (state.current.path !== path) {
196
221
  return;
197
222
  }
198
- if (state.current.data === void 0) {
199
- throw new Error("Attempt to update data before it has been loaded");
200
- }
201
- state.current.data = update(state.current.data);
223
+ state.current.data = data2;
202
224
  state.current.lastUpdatedMillis = Date.now();
203
225
  state.current.state = { state: "dirty" };
204
226
  saveData();
205
227
  updateDataFromState();
206
228
  },
207
- [path]
229
+ [saveData, path]
230
+ );
231
+ const updateData = _react.useMemo.call(void 0,
232
+ () => (update) => {
233
+ if (state.current.data === void 0) {
234
+ throw new Error("Attempt to update data before it has been loaded");
235
+ }
236
+ setDataTo(update(state.current.data));
237
+ },
238
+ [setDataTo]
239
+ );
240
+ const resetData = _react.useMemo.call(void 0,
241
+ () => () => setDataTo(defaultValue),
242
+ [setDataTo, defaultValue]
208
243
  );
209
244
  return {
210
245
  data,
211
246
  updateData,
247
+ resetData,
212
248
  saveData
213
249
  };
214
250
  }
@@ -223,6 +259,9 @@ function createDataFileDefinition({
223
259
  updateData: () => {
224
260
  throw new Error("Data file provider not used");
225
261
  },
262
+ resetData: () => {
263
+ throw new Error("Data file provider not used");
264
+ },
226
265
  saveData: () => {
227
266
  throw new Error("Data file provider not used");
228
267
  },
@@ -245,7 +284,7 @@ function createDataFileDefinition({
245
284
  onError,
246
285
  children
247
286
  }) => {
248
- const { data, updateData, saveData } = useDataFile2({
287
+ const { data, updateData, resetData, saveData } = useDataFile2({
249
288
  path,
250
289
  onPathChange,
251
290
  onError
@@ -256,6 +295,7 @@ function createDataFileDefinition({
256
295
  status: data.status,
257
296
  lastUpdatedMillis: data.lastUpdatedMillis,
258
297
  updateData,
298
+ resetData,
259
299
  saveData,
260
300
  error: data.status === "error" ? data.error : void 0
261
301
  }),
package/dist/data.mjs CHANGED
@@ -4,6 +4,7 @@ import {
4
4
 
5
5
  // src/data.tsx
6
6
  import { promises as fs } from "fs";
7
+ import { randomUUID } from "crypto";
7
8
  import {
8
9
  useMemo,
9
10
  useContext,
@@ -28,13 +29,27 @@ function useDataFile(dataFile, usage) {
28
29
  return dataFile.useDataFile(usage);
29
30
  }
30
31
  var ArcaneDataFileError = class extends Error {
31
- constructor(message, operation, path, cause) {
32
+ constructor(message, operation, path, contents, cause) {
32
33
  super(message, { cause });
33
34
  this.operation = operation;
34
35
  this.path = path;
36
+ this.contents = contents;
35
37
  this.name = `ArcaneDataFileError(${operation})`;
36
38
  }
37
39
  };
40
+ function stripUtf8Bom(data) {
41
+ return data.charCodeAt(0) === 65279 ? data.slice(1) : data;
42
+ }
43
+ async function writeFileAtomically(path, data) {
44
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
45
+ try {
46
+ await fs.writeFile(tempPath, data, "utf8");
47
+ await fs.rename(tempPath, path);
48
+ } catch (error) {
49
+ await fs.rm(tempPath, { force: true }).catch(() => void 0);
50
+ throw error;
51
+ }
52
+ }
38
53
  function useDataFileCore({
39
54
  schema,
40
55
  defaultValue,
@@ -45,6 +60,7 @@ function useDataFileCore({
45
60
  const log = useLogger();
46
61
  const state = useRef({
47
62
  initialized: false,
63
+ saveChain: Promise.resolve(),
48
64
  path: null,
49
65
  data: void 0,
50
66
  previousData: void 0,
@@ -60,6 +76,7 @@ function useDataFileCore({
60
76
  const error = new ArcaneDataFileError(
61
77
  "Cannot change schema or defaultValue after initialization",
62
78
  "usage",
79
+ null,
63
80
  null
64
81
  );
65
82
  onError?.(error);
@@ -110,11 +127,15 @@ function useDataFileCore({
110
127
  if (!currentPath || currentData === void 0) {
111
128
  return;
112
129
  }
113
- try {
114
- const json = JSON.stringify(currentData, null, 2);
130
+ const json = JSON.stringify(currentData, null, 2);
131
+ const queuedSave = state.current.saveChain.catch(() => void 0).then(async () => {
115
132
  await fs.mkdir(dirname(currentPath), { recursive: true });
116
- await fs.writeFile(currentPath, json, "utf8");
117
- if (state.current.path === currentPath && state.current.data === currentData) {
133
+ await writeFileAtomically(currentPath, json);
134
+ });
135
+ try {
136
+ state.current.saveChain = queuedSave;
137
+ await queuedSave;
138
+ if (state.current.path === currentPath && state.current.saveChain === queuedSave) {
118
139
  state.current.state = { state: "saved" };
119
140
  }
120
141
  } catch (cause) {
@@ -122,10 +143,11 @@ function useDataFileCore({
122
143
  `Error saving data file to path: ${currentPath}`,
123
144
  "save",
124
145
  currentPath,
146
+ null,
125
147
  cause
126
148
  );
127
149
  onError?.(error);
128
- if (state.current.path === currentPath && state.current.data === currentData) {
150
+ if (state.current.path === currentPath && state.current.saveChain === queuedSave) {
129
151
  state.current.state = { state: "error", error };
130
152
  updateDataFromState();
131
153
  }
@@ -152,8 +174,10 @@ function useDataFileCore({
152
174
  state: "saved"
153
175
  }
154
176
  };
177
+ let contents = null;
155
178
  fs.readFile(path, "utf8").then((data2) => {
156
- const parsedData = schema.parse(JSON.parse(data2));
179
+ contents = data2;
180
+ const parsedData = schema.parse(JSON.parse(stripUtf8Bom(data2)));
157
181
  if (state.current.path === path) {
158
182
  state.current.data = parsedData;
159
183
  state.current.lastUpdatedMillis = Date.now();
@@ -182,6 +206,7 @@ function useDataFileCore({
182
206
  `Error loading data file at path: ${path}`,
183
207
  "load",
184
208
  path,
209
+ contents,
185
210
  err
186
211
  );
187
212
  onError?.(error);
@@ -190,25 +215,36 @@ function useDataFileCore({
190
215
  updateDataFromState();
191
216
  });
192
217
  }, [path, onPathChange]);
193
- const updateData = useMemo(
194
- () => (update) => {
218
+ const setDataTo = useMemo(
219
+ () => (data2) => {
195
220
  if (state.current.path !== path) {
196
221
  return;
197
222
  }
198
- if (state.current.data === void 0) {
199
- throw new Error("Attempt to update data before it has been loaded");
200
- }
201
- state.current.data = update(state.current.data);
223
+ state.current.data = data2;
202
224
  state.current.lastUpdatedMillis = Date.now();
203
225
  state.current.state = { state: "dirty" };
204
226
  saveData();
205
227
  updateDataFromState();
206
228
  },
207
- [path]
229
+ [saveData, path]
230
+ );
231
+ const updateData = useMemo(
232
+ () => (update) => {
233
+ if (state.current.data === void 0) {
234
+ throw new Error("Attempt to update data before it has been loaded");
235
+ }
236
+ setDataTo(update(state.current.data));
237
+ },
238
+ [setDataTo]
239
+ );
240
+ const resetData = useMemo(
241
+ () => () => setDataTo(defaultValue),
242
+ [setDataTo, defaultValue]
208
243
  );
209
244
  return {
210
245
  data,
211
246
  updateData,
247
+ resetData,
212
248
  saveData
213
249
  };
214
250
  }
@@ -223,6 +259,9 @@ function createDataFileDefinition({
223
259
  updateData: () => {
224
260
  throw new Error("Data file provider not used");
225
261
  },
262
+ resetData: () => {
263
+ throw new Error("Data file provider not used");
264
+ },
226
265
  saveData: () => {
227
266
  throw new Error("Data file provider not used");
228
267
  },
@@ -245,7 +284,7 @@ function createDataFileDefinition({
245
284
  onError,
246
285
  children
247
286
  }) => {
248
- const { data, updateData, saveData } = useDataFile2({
287
+ const { data, updateData, resetData, saveData } = useDataFile2({
249
288
  path,
250
289
  onPathChange,
251
290
  onError
@@ -256,6 +295,7 @@ function createDataFileDefinition({
256
295
  status: data.status,
257
296
  lastUpdatedMillis: data.lastUpdatedMillis,
258
297
  updateData,
298
+ resetData,
259
299
  saveData,
260
300
  error: data.status === "error" ? data.error : void 0
261
301
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcanejs/react-toolkit",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "description": "Build web-accessible control interfaces for your long-running Node.js processes",
6
6
  "keywords": [