offline-cloudinary 2.0.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.0] - 2025-12-18
9
+
10
+ ### Added
11
+
12
+ - **In-memory caching system**: File mappings are now cached in memory for dramatically faster operations
13
+ - **Dirty flag tracking**: Efficient cache invalidation system that only syncs to disk when changes occur
14
+ - **Automatic periodic sync**: Background task (500ms intervals) automatically persists cache changes to disk
15
+ - **Graceful shutdown**: Process cleanup handler that ensures all in-memory changes are synced to disk on exit
16
+
17
+ ### Changed
18
+
19
+ - `upload()` and `destroy()` operations now use in-memory cache for improved performance
20
+ - `initialise()` method now handles lazy initialization and cache management
21
+ - Improved resource management with automatic interval cleanup
22
+
23
+ ### Performance
24
+
25
+ - Significantly reduced disk I/O operations
26
+ - Faster file operations through in-memory lookups
27
+ - Minimal overhead with periodic batch writes to disk
28
+
29
+ ### Fully Backward Compatible
30
+
31
+ All v2.0.0 APIs remain unchanged. The caching system is transparent to end users.
32
+
8
33
  ## [2.0.0] - 2025-12-15
9
34
 
10
35
  ### Breaking Changes
package/index.js CHANGED
@@ -13,8 +13,13 @@ const startEmulator = () => {
13
13
  const portNumber = process.env.CLOUDINARY_OFFLINE_PORT;
14
14
  if (!portNumber)
15
15
  throw new Error("Please set CLOUDINARY_OFFLINE_PORT in your .env file");
16
- app.listen(portNumber, () =>
16
+ app.listen(portNumber, () =>{
17
17
  console.log("Offline Cloudinary running on port", portNumber)
18
+ process.on("SIGINT", async()=>{
19
+ if (offlineCloudinary.syncActive) clearInterval(offlineCloudinary.syncActive)
20
+ await offlineCloudinary.syncToDisk()
21
+ })
22
+ }
18
23
  );
19
24
  };
20
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offline-cloudinary",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "An offline Cloudinary-like file manager with HTTP server emulator for local uploads, deletions, and testing without internet access.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -8,6 +8,29 @@ class OfflineCloudinary {
8
8
  throw new Error("Please set CLOUDINARY_OFFLINE_PATH in your .env file");
9
9
  }
10
10
  this.rootPath = process.env.CLOUDINARY_OFFLINE_PATH;
11
+ this.initialised = false;
12
+ this.mappingsInMemory = { isDirty: false };
13
+ this.syncActive = false;
14
+ }
15
+
16
+ async initialise() {
17
+ if (this.initialised) return;
18
+ const filePath = path.join(this.rootPath, "uploads.json");
19
+ await fs.access(filePath).catch(() => fs.writeFile(filePath, "{}"));
20
+ const data = await fs.readFile(filePath, "utf-8");
21
+ this.mappingsInMemory = { ...JSON.parse(data), isDirty: false };
22
+ this.initialised = true;
23
+ this.syncActive = setInterval(() => this.syncToDisk(), 500);
24
+ }
25
+
26
+ async syncToDisk() {
27
+ if (!this.mappingsInMemory.isDirty) return;
28
+ const mappingsCopy = { ...this.mappingsInMemory, isDirty: false };
29
+ await fs.writeFile(
30
+ path.join(this.rootPath, "uploads.json"),
31
+ JSON.stringify(mappingsCopy)
32
+ );
33
+ this.mappingsInMemory.isDirty = false;
11
34
  }
12
35
 
13
36
  /**
@@ -17,9 +40,8 @@ class OfflineCloudinary {
17
40
  * @returns Cloudinary-like response
18
41
  */
19
42
  async upload(tempFilePath, options = {}) {
20
- const portNumber = process.env.CLOUDINARY_OFFLINE_PORT;
21
- if (!portNumber)
22
- throw new Error("Please set CLOUDINARY_OFFLINE_PORT in your .env file");
43
+ await this.initialise();
44
+ const portNumber = process.env.CLOUDINARY_OFFLINE_PORT || 3500;
23
45
  await fs.access(tempFilePath).catch(() => {
24
46
  throw new Error(`File not found: ${tempFilePath}`);
25
47
  });
@@ -47,12 +69,8 @@ class OfflineCloudinary {
47
69
 
48
70
  const uploadId = crypto.randomUUID();
49
71
 
50
- const data = await fs.readFile("uploads.json", "utf-8");
51
-
52
- const mappings = JSON.parse(data);
53
- mappings[uploadId] = finalPath;
54
-
55
- await fs.writeFile("uploads.json", JSON.stringify(mappings));
72
+ this.mappingsInMemory[uploadId] = finalPath;
73
+ this.mappingsInMemory.isDirty = true;
56
74
 
57
75
  // Return Cloudinary-like response
58
76
  return {
@@ -83,10 +101,14 @@ class OfflineCloudinary {
83
101
  * @returns {object} { result: "ok" } if deleted or { result: "not found" }
84
102
  */
85
103
  async destroy(public_id) {
104
+ await this.initialise();
86
105
  const uploadId = public_id;
87
- const data = await fs.readFile("uploads.json", "utf-8");
88
- const mappings = JSON.parse(data);
89
- await fs.unlink(mappings[uploadId]);
106
+ const filePath = this.mappingsInMemory[uploadId];
107
+ if (filePath) {
108
+ await fs.unlink(filePath);
109
+ delete this.mappingsInMemory[uploadId];
110
+ this.mappingsInMemory.isDirty = true;
111
+ }
90
112
  return { result: "ok" };
91
113
  }
92
114
 
@@ -97,6 +119,7 @@ class OfflineCloudinary {
97
119
  async clearStorage() {
98
120
  await fs.rm(this.rootPath, { recursive: true, force: true });
99
121
  await fs.mkdir(this.rootPath);
122
+ this.mappingsInMemory = { isDirty: false };
100
123
  return { result: "ok" };
101
124
  }
102
125
  }