mongofire 6.5.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/CHANGELOG.md ADDED
@@ -0,0 +1,93 @@
1
+ # Changelog
2
+
3
+ All notable changes to MongoFire are documented here.
4
+
5
+ ## [6.5.0] — 2026-03-08
6
+
7
+ ### Fixed — Critical
8
+
9
+ - **Batch upload data corruption** — operations in the same upload batch could be incorrectly acknowledged, causing sync state to diverge silently. Each operation is now acknowledged individually with its correct status
10
+ - **Atlas connection pool leak** — a failed Atlas health check left the underlying connection open, leaking pool connections and causing every subsequent sync to fail with a connection error. Connection is now fully released on failure
11
+ - **Delete overwrites concurrent update** — a queued delete could silently overwrite a document that another device had updated in the meantime. This is now detected and surfaced as a `conflict` event instead of data loss
12
+ - **Date fields corrupted on sync** — `Date` values in synced documents were being converted to plain ISO strings, breaking date-range queries (`$gt`, `$lt`) on the receiving device. BSON types are now preserved correctly across sync
13
+ - **`syncOwner` error grants access to all data** — if the `syncOwner` function threw an error, MongoFire silently fell back to syncing all owners. Now the sync is aborted and an `error` event is emitted, preventing unintended data access
14
+
15
+ ### Fixed — Medium
16
+
17
+ - **Docker / cloned VM device ID collision** — instances sharing the same MAC address and hostname (e.g. Docker replicas, cloned VMs) generated identical device IDs, causing changes from those devices to be silently skipped on other instances. Device IDs are now always unique
18
+ - **`updateOne` hook could track wrong document** — the change tracking hook re-read the document after the update using the original filter, which could match a different document in a concurrent write scenario. The hook now locks on the specific document ID before the update runs
19
+ - **Bootstrap restart from scratch on failure** — if a bootstrap sync was interrupted (crash, network drop), the next start re-downloaded all documents from the beginning. Bootstrap now resumes from the last completed checkpoint
20
+ - **Sync state and device registry created on Atlas** — two local-only internal collections were being created on Atlas unnecessarily, consuming storage and triggering index maintenance. They are now created on local only
21
+
22
+ ### Fixed — Minor
23
+
24
+ - **Shared internal object could be accidentally mutated** — an internal configuration object was shared across calls; mutating it in one place could silently affect all subsequent calls. It is now immutable
25
+ - **Multiple app instances share one signal handler** — only the first MongoFire instance received `SIGINT`/`SIGTERM` cleanup. Each instance now manages its own handler and removes it on `stop()`, preventing memory leaks in test environments
26
+ - **`require('mongofire/plugin')` API inconsistency** — the direct plugin import had a different call signature to `mongofire.plugin()`. Both now work the same way, with a `.factory()` helper added for parity
27
+ - **Collection name with special characters causes key collision** — two different collections could produce identical internal keys, leading to metadata corruption. Collection names are now validated at startup with a clear error message
28
+
29
+ ### Fixed — Security
30
+
31
+ - **Hardware fingerprint stored on Atlas** — a derived MAC address value was being stored in Atlas, accessible to anyone with database read access. This constituted an unnecessary hardware fingerprint. The field has been removed
32
+ - **Arbitrary collection names accepted** — collection names were not validated, allowing names that could interfere with MongoFire's internal collections. Names are now validated at startup: no special characters that cause key collisions, no reserved prefixes
33
+ - **Oversized documents cause cryptic failure** — documents close to MongoDB's 16MB limit failed with an unhelpful low-level error. MongoFire now checks document size before writing and throws a clear, actionable message
34
+ - **Manual `sync()` calls not rate-limited** — a runaway loop calling `sync()` in rapid succession could hammer Atlas with back-to-back requests. Rapid successive calls are now throttled automatically
35
+
36
+ ### Fixed — Performance
37
+
38
+ - **Checksum computed on every document** — a cryptographic checksum was calculated for every document on every sync cycle regardless of whether verification was enabled. It is now skipped by default and only computed when `MONGOFIRE_VERIFY_REMOTE=1`
39
+ - **Bulk update tracking loaded all IDs into memory** — tracking changes for a large `updateMany` operation materialised the full list of matching IDs in memory (100MB+ for millions of documents). The hook now uses a streaming cursor instead
40
+ - **Too many database round-trips during upload** — uploading a large backlog of pending changes required many more database queries than necessary. Batch size has been increased significantly, reducing round-trips by ~60%
41
+
42
+ ### Fixed — Reliability
43
+
44
+ - **Conflicts were silently swallowed** — when a version conflict was detected during upload, it was recorded internally but never surfaced to the application. MongoFire now emits a `conflict` event with structured data so you can respond to it
45
+ - **Same-millisecond changes could be missed** — the delta sync cursor used only a timestamp, so two changes with the exact same timestamp could result in one being skipped on the next sync. The cursor now uses a compound position that eliminates this gap
46
+
47
+ ### Added
48
+
49
+ - **`conflict` event** — emitted when a version conflict is detected during upload, with `{ collection, docId, localVersion, remoteVersion, op }` payload. See Events section in the README
50
+ - **`ConflictData` TypeScript interface** — fully typed payload for the `conflict` event
51
+ - **`mongofire/plugin` factory export** — `require('mongofire/plugin').factory(name, opts)` matches the `mongofire.plugin()` signature for direct use without the singleton
52
+ - **`MONGOFIRE_COLLECTION_CONCURRENCY` env var** — configure how many collections sync in parallel at runtime (default: 4)
53
+ - **Startup validation for collection names** — invalid names fail immediately with a clear message instead of silently corrupting data at sync time
54
+
55
+ ### Changed
56
+
57
+ - `syncInterval` default is now `30000`ms (polling mode) or `5000`ms (when `realtime: true`)
58
+ - `clean()` default changed from 30 days → **7 days**, matching the Atlas-side TTL so both sides stay consistent
59
+ - `plugin()` `concurrency` option is now actually used (was accepted but ignored in previous versions; default: `8`)
60
+ - Device IDs now include random bytes — existing device records are preserved on startup, so this only affects new installations
61
+
62
+ ---
63
+
64
+ ## [6.2.0] — 2026-03-08
65
+
66
+ ### Fixed — Critical
67
+ - **`start()` concurrent safety** — multiple simultaneous `start()` calls now share one init promise instead of racing
68
+ - **Bootstrap re-trigger bug** — an empty collection no longer forces a full re-bootstrap of all collections
69
+ - **Silent change tracking errors** — errors in the Mongoose hooks are now logged instead of swallowed
70
+ - **Realtime sync not working** — change stream pipeline fix; was silently delivering zero events on most Atlas clusters
71
+
72
+ ### Fixed — Medium
73
+ - **`deleteMany` OOM risk** — plugin now streams and batches docs before deletion; removes 10,000-doc silent cap
74
+ - **Session not forwarded in `updateOne` and `deleteOne` hooks** — reads now occur within the same transaction context
75
+
76
+ ### Added
77
+ - Full TypeScript declarations (`types/index.d.ts`) with typed events, config, and result interfaces
78
+ - `require('mongofire/plugin')` subpath export
79
+ - Max retry limit (10 attempts) for permanently failing operations
80
+
81
+ ---
82
+
83
+ ## [6.1.0] — initial release
84
+
85
+ - Offline-first sync with Local MongoDB + Atlas
86
+ - Mongoose plugin with hooks for save, update, delete operations
87
+ - Bootstrap + delta oplog sync
88
+ - Automatic conflict resolution (version vector + timestamp + deviceId tiebreaker)
89
+ - Real-time sync via Atlas Change Streams with polling fallback
90
+ - `npx mongofire init / status / clean` CLI
91
+ - Exponential backoff retry with jitter
92
+ - TTL index auto-cleanup of old sync records
93
+ - Multi-tenant `syncOwner` support
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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 deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ 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 all
13
+ 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 FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # 🔥 MongoFire
2
+
3
+ > **Offline-first MongoDB sync** — Local + Atlas feel like ONE database.
4
+ > Automatic conflict resolution, Mongoose plugin, zero boilerplate.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/mongofire)](https://www.npmjs.com/package/mongofire)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
+
10
+ ---
11
+
12
+ ## What it does
13
+
14
+ MongoFire keeps a **local MongoDB** and **MongoDB Atlas** in sync automatically.
15
+
16
+ - Your app reads/writes to local MongoDB — always fast, works offline
17
+ - MongoFire tracks every change and uploads to Atlas when online
18
+ - Downloads remote changes from Atlas in the background
19
+ - If you go offline, changes queue up and sync when you reconnect
20
+ - Conflicts resolved automatically (last-writer-wins with version vectors)
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install mongofire
28
+ ```
29
+
30
+ **Peer dependencies** (install the ones you use):
31
+
32
+ ```bash
33
+ npm install mongodb mongoose
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Quick Start
39
+
40
+ ### 1. Init config files
41
+
42
+ ```bash
43
+ npx mongofire init
44
+ ```
45
+
46
+ This creates:
47
+ - `.env` — MongoDB connection strings
48
+ - `mongofire.config.js` — which collections to sync
49
+ - `mongofire.js` — app entry point
50
+
51
+ ### 2. Fill in `.env`
52
+
53
+ ```env
54
+ ATLAS_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/
55
+ LOCAL_URI=mongodb://127.0.0.1:27017
56
+ DB_NAME=myapp
57
+ ```
58
+
59
+ ### 3. Start sync in your app
60
+
61
+ ```js
62
+ // CommonJS
63
+ const mongofire = require('mongofire');
64
+ const config = require('./mongofire.config');
65
+
66
+ await mongofire.start(config);
67
+
68
+ // ESM
69
+ import mongofire from 'mongofire';
70
+ import config from './mongofire.config.js';
71
+
72
+ await mongofire.start(config);
73
+ ```
74
+
75
+ ### 4. Add the plugin to your Mongoose schema
76
+
77
+ ```js
78
+ const mongofire = require('mongofire');
79
+
80
+ const UserSchema = new mongoose.Schema({
81
+ name: String,
82
+ email: String,
83
+ userId: mongoose.Types.ObjectId,
84
+ });
85
+
86
+ // Track changes on the 'users' collection
87
+ // ownerField: field used for per-user data isolation (multi-tenant)
88
+ UserSchema.plugin(mongofire.plugin('users', { ownerField: 'userId' }));
89
+
90
+ const User = mongoose.model('User', UserSchema);
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Config Options
96
+
97
+ | Option | Type | Default | Description |
98
+ |---|---|---|---|
99
+ | `collections` | `string[]` | *required* | Collection names to sync |
100
+ | `localUri` | `string` | `mongodb://localhost:27017` | Local MongoDB URI |
101
+ | `atlasUri` | `string` | `null` | Atlas connection string |
102
+ | `dbName` | `string` | `'mongofire'` | Database name |
103
+ | `syncInterval` | `number` | `30000` (polling) / `5000` (realtime) | Polling interval in ms |
104
+ | `batchSize` | `number` | `200` | Docs per upload/download batch |
105
+ | `syncOwner` | `string\|fn` | `'*'` | Owner key for multi-tenant filtering. If a function, throwing will **abort** the sync to prevent unintended data access |
106
+ | `realtime` | `boolean` | `false` | Use Atlas Change Streams for instant sync |
107
+ | `onSync` | `function` | `null` | Called after each sync cycle |
108
+ | `onError` | `function` | `null` | Called on sync errors |
109
+
110
+ ---
111
+
112
+ ## Events
113
+
114
+ ```js
115
+ mongofire.on('ready', () => console.log('MongoFire started'));
116
+ mongofire.on('online', () => console.log('Atlas connected'));
117
+ mongofire.on('offline', () => console.log('Working locally'));
118
+ mongofire.on('sync', (r) => console.log('Sync result:', r));
119
+ mongofire.on('conflict', (c) => console.warn('Conflict detected:', c));
120
+ mongofire.on('realtimeStarted', () => console.log('Change streams active'));
121
+ mongofire.on('stopped', () => console.log('Shut down cleanly'));
122
+ mongofire.on('error', (e) => console.error('Sync error:', e));
123
+ ```
124
+
125
+ ### `conflict` event
126
+
127
+ Emitted when a local write conflicts with a concurrent remote change. Use this to notify users or trigger a re-fetch:
128
+
129
+ ```js
130
+ mongofire.on('conflict', ({ collection, docId, localVersion, remoteVersion, op }) => {
131
+ console.warn(`Conflict on ${collection}/${docId}`);
132
+ console.warn(` Local was at version ${localVersion}, Atlas is at ${remoteVersion}`);
133
+ // Fetch the latest remote doc and re-apply your changes if needed
134
+ });
135
+ ```
136
+
137
+ ---
138
+
139
+ ## API
140
+
141
+ ### `mongofire.start(config)` → `Promise<MongoFire>`
142
+ Connect and start background sync. Concurrent calls are safe — all await the same init.
143
+
144
+ ### `mongofire.stop(timeoutMs?)` → `Promise<void>`
145
+ Flush pending ops, wait for active sync, close all connections. Default timeout: 10 seconds.
146
+
147
+ ### `mongofire.sync(type?)` → `Promise<SyncResult>`
148
+ Manually trigger a sync. `type` can be `'required'` (default) or `'all'`. Rapid successive calls are throttled automatically.
149
+
150
+ ### `mongofire.status()` → `Promise<SyncStatus>`
151
+ Get pending op counts and online/realtime status.
152
+
153
+ ### `mongofire.clean(days?)` → `Promise<number>`
154
+ Delete old sync records older than `days` days (default: **7**). Returns count of deleted records.
155
+
156
+ ### `mongofire.plugin(collectionName, options?)`
157
+ Returns a Mongoose schema plugin. Options:
158
+
159
+ | Option | Type | Default | Description |
160
+ |---|---|---|---|
161
+ | `ownerField` | `string` | `null` | Dot-path to owner key field (e.g. `'userId'` or `'org._id'`) |
162
+ | `batchSize` | `number` | `200` | Batch size for bulk operations |
163
+ | `concurrency` | `number` | `8` | Concurrent change tracking calls per batch |
164
+
165
+ ---
166
+
167
+ ## Using the plugin directly
168
+
169
+ If you prefer not to use the MongoFire singleton, import the plugin directly:
170
+
171
+ ```js
172
+ // Raw Mongoose plugin
173
+ const mongofirePlugin = require('mongofire/plugin');
174
+ UserSchema.plugin(mongofirePlugin, { collection: 'users', ownerField: 'userId' });
175
+
176
+ // Or use the factory helper (same signature as mongofire.plugin())
177
+ const { factory } = require('mongofire/plugin');
178
+ UserSchema.plugin(factory('users', { ownerField: 'userId' }));
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Real-Time Sync
184
+
185
+ Enable instant sync via MongoDB Atlas Change Streams:
186
+
187
+ ```js
188
+ await mongofire.start({
189
+ // ...
190
+ realtime: true, // requires Atlas cluster or local replica set
191
+ });
192
+ ```
193
+
194
+ If Change Streams are unavailable, MongoFire automatically falls back to polling — no crash, no config needed.
195
+
196
+ ---
197
+
198
+ ## Multi-Tenant Usage
199
+
200
+ ```js
201
+ // Sync only data belonging to a specific user
202
+ await mongofire.start({
203
+ collections: ['notes', 'tasks'],
204
+ syncOwner: () => currentUser.id, // dynamic — re-evaluated on each sync cycle
205
+ // ...
206
+ });
207
+ ```
208
+
209
+ > **Note:** If `syncOwner` throws, the sync is **aborted** and an `error` event is emitted. This prevents unintended access to other users' data.
210
+
211
+ ---
212
+
213
+ ## CLI
214
+
215
+ ```bash
216
+ # Create config files in current project
217
+ npx mongofire init
218
+
219
+ # Force overwrite existing config
220
+ npx mongofire init --force
221
+
222
+ # Check pending sync status
223
+ npx mongofire status
224
+
225
+ # Delete old sync records
226
+ npx mongofire clean
227
+ npx mongofire clean --days=7
228
+ ```
229
+
230
+ ---
231
+
232
+ ## TypeScript
233
+
234
+ Full TypeScript support is included:
235
+
236
+ ```ts
237
+ import mongofire, { MongoFire, SyncConfig, SyncResult, ConflictData } from 'mongofire';
238
+
239
+ const config: SyncConfig = {
240
+ collections: ['users'],
241
+ atlasUri: process.env.ATLAS_URI,
242
+ realtime: true,
243
+ };
244
+
245
+ await mongofire.start(config);
246
+
247
+ mongofire.on('sync', (result: SyncResult) => {
248
+ console.log(`Uploaded: ${result.uploaded}, Downloaded: ${result.downloaded}`);
249
+ });
250
+
251
+ mongofire.on('conflict', (data: ConflictData) => {
252
+ console.warn(`Conflict on ${data.collection}/${data.docId}`);
253
+ });
254
+ ```
255
+
256
+ ---
257
+
258
+ ## How it Works
259
+
260
+ 1. **Change Tracking** — Every `save`/`update`/`delete` via Mongoose hooks is recorded locally before syncing to Atlas
261
+ 2. **Upload** — Pending local changes are uploaded to Atlas inside MongoDB transactions, with automatic retry and idempotency
262
+ 3. **Download (Bootstrap)** — First sync streams all remote docs in batches. Resumable — a crash mid-bootstrap picks up from where it left off
263
+ 4. **Download (Delta)** — Subsequent syncs fetch only changes newer than the last seen position, with no gaps even at millisecond boundaries
264
+ 5. **Conflict Resolution** — Version number → timestamp → deviceId tiebreaker (deterministic, no coin flip)
265
+ 6. **Offline** — All reads/writes work locally. Changes queue up and upload automatically when Atlas reconnects
266
+
267
+ ---
268
+
269
+ ## Collection Name Rules
270
+
271
+ Collection names passed to `mongofire.plugin()` and `config.collections` must:
272
+ - Start with a letter or digit
273
+ - Contain only letters, digits, underscores (`_`), hyphens (`-`), or dots (`.`)
274
+ - **Not** contain a colon (`:`)
275
+ - **Not** start with `_mf_` (reserved for MongoFire internal use)
276
+
277
+ Invalid names throw a clear error at startup.
278
+
279
+ ---
280
+
281
+ ## Environment Variables
282
+
283
+ | Variable | Default | Description |
284
+ |---|---|---|
285
+ | `MONGOFIRE_VERIFY_REMOTE` | `0` | Set to `1` to verify each uploaded doc with a checksum round-trip |
286
+ | `MONGOFIRE_COLLECTION_CONCURRENCY` | `4` | Number of collections synced in parallel |
287
+
288
+ ---
289
+
290
+ ## License
291
+
292
+ MIT — see [LICENSE](LICENSE)