mongofire 6.5.3 → 6.5.6

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.
@@ -1,323 +1,278 @@
1
1
  import { EventEmitter } from 'events';
2
2
 
3
- // ─── Configuration ────────────────────────────────────────────────────────────
4
-
5
- export interface SyncConfig {
6
- /** Local MongoDB URI. Default: 'mongodb://localhost:27017' */
7
- localUri?: string;
8
- /** MongoDB Atlas connection string. Required for sync; omit for local-only mode. */
9
- atlasUri?: string;
10
- /** Database name. Default: 'mongofire' */
11
- dbName?: string;
12
- /** Collection names to sync. Required. */
13
- collections: string[];
14
- /**
15
- * Polling interval in milliseconds.
16
- * Default: 5000 (5s) when realtime:true, 30000 (30s) when realtime:false
17
- */
18
- syncInterval?: number;
19
- /** Upload/download batch size. Default: 200 */
20
- batchSize?: number;
21
- /**
22
- * Owner key for multi-tenant filtering.
23
- * Pass '*' to sync all owners (default), a string for a specific owner,
24
- * or a function that returns the current owner key.
25
- */
26
- syncOwner?: string | (() => string);
27
- /**
28
- * Enable real-time sync via Atlas Change Streams.
29
- * Requires a MongoDB Atlas cluster or a local replica set.
30
- * Falls back to polling if unavailable.
31
- * Default: false
32
- */
33
- realtime?: boolean;
34
- /** Called after each successful sync cycle. */
35
- onSync?: (result: SyncResult) => void;
36
- /** Called when a sync cycle throws an unexpected error. */
37
- onError?: (err: Error) => void;
38
- /**
39
- * Run a reconciliation scan on every start() to recover writes lost due to
40
- * a crash between the Mongoose data write and the changetrack insertOne.
41
- * Default: true. Set false only if startup latency is critical.
42
- */
43
- reconcileOnStart?: boolean;
44
- /**
45
- * Include Phase 2 of reconciliation: scan the data collection for documents
46
- * with no _mf_docmeta entry (written before MongoFire was initialized, or
47
- * crash before recordChange() was called at all).
48
- * Default: true. Set false to run only the fast orphaned-meta check (Phase 1).
49
- */
50
- reconcileFullScan?: boolean;
3
+ // ─── Utility types ────────────────────────────────────────────────────────────
4
+ type MaybePromise<T> = T | Promise<T>;
5
+ type Timestamp = Date | number;
6
+ type DocId = string;
7
+ type CircuitBreakerState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
8
+
9
+ // ─── JSON Patch / Diff ───────────────────────────────────────────────────────
10
+ export interface JsonPatchOp { op: 'add'|'remove'|'replace'|'move'|'copy'|'test'; path: string; value?: unknown; from?: string; }
11
+ export interface PatchPayload { _mf_patch: JsonPatchOp[]; _mf_full: Record<string,unknown>; _mf_fieldCount: number; _mf_changedFields: string[]; _mf_patchSize: number; _mf_fullSize: number; _mf_field_meta?: FieldMeta; }
12
+ export interface DiffResult { patch: JsonPatchOp[]; fieldCount: number; changedFields: string[]; }
13
+ export interface CompressResult { data: unknown; compressed: boolean; encoding?: 'gzip+base64'; originalSize: number; compressedSize: number; }
14
+ export declare function computeDiff(original: Record<string,unknown>, updated: Record<string,unknown>): DiffResult | null;
15
+ export declare function buildUpdatePayload(doc: Record<string,unknown>, diffResult: DiffResult | null): Record<string,unknown> | PatchPayload;
16
+ export declare function applyPatch(baseDoc: Record<string,unknown> | null, payload: Record<string,unknown> | PatchPayload): Record<string,unknown>;
17
+ export declare function isPatchPayload(payload: unknown): payload is PatchPayload;
18
+ export declare function compressPayload(payload: unknown): Promise<CompressResult>;
19
+ export declare function decompressPayload(data: string | unknown, compressed: boolean): Promise<unknown>;
20
+
21
+ // ─── Field-level Merge ────────────────────────────────────────────────────────
22
+ export interface FieldMetaEntry { ts: number; deviceId: string; version: number; }
23
+ export type FieldMeta = Record<string, FieldMetaEntry>;
24
+ export interface MergeResult { merged: Record<string,unknown>; resolvedFields: Record<string,'local'|'remote'>; conflictedFields: Record<string,{local:unknown;remote:unknown;winner:'local'|'remote'}>; }
25
+ export declare function buildFieldMeta(patch: object[], timestamp: Timestamp, deviceId: string, version: number): FieldMeta;
26
+ export declare function mergeFields(baseDoc: Record<string,unknown>, localMeta: {_mf_field_meta?: FieldMeta}, remotePayload: Record<string,unknown>, remoteMeta: {timestamp:Timestamp;deviceId:string;version:number;field_meta?:FieldMeta}): MergeResult;
27
+ export declare function hasFieldOverlap(opA: {payload?:{_mf_field_meta?:FieldMeta;_mf_patch?:object[]}}, opB: {payload?:{_mf_field_meta?:FieldMeta;_mf_patch?:object[]}}): boolean;
28
+ export declare function mergeScore(mergeResult: Pick<MergeResult,'resolvedFields'|'conflictedFields'>): number;
29
+
30
+ // ─── Schema Manager ───────────────────────────────────────────────────────────
31
+ export type MigrationFn<T=Record<string,unknown>> = (doc: T) => MaybePromise<T>;
32
+ export type ValidatorFn<T=Record<string,unknown>> = (doc: T) => string[];
33
+ export interface ValidationResult { valid: boolean; errors: string[]; }
34
+ export interface SchemaRegistryEntry { collection: string; currentVersion: number; versions: number[]; updatedAt: Date; }
35
+ export declare class SchemaManager {
36
+ register<T=Record<string,unknown>>(collection: string, version: number, migrateFn: MigrationFn<T>): void;
37
+ registerValidator<T=Record<string,unknown>>(collection: string, version: number, validateFn: ValidatorFn<T>): void;
38
+ currentVersion(collection: string): number;
39
+ needsMigration(collection: string, doc: Record<string,unknown>): boolean;
40
+ migrate<T=Record<string,unknown>>(collection: string, doc: T): Promise<T & {_mf_schemaVersion:number}>;
41
+ migrateBatch<T=Record<string,unknown>>(collection: string, docs: T[], concurrency?: number): Promise<Array<T & {_mf_schemaVersion:number}>>;
42
+ validate(collection: string, doc: Record<string,unknown>): ValidationResult;
43
+ persistRegistry(conn?: unknown): Promise<void>;
44
+ getRegistry(conn?: unknown): Promise<SchemaRegistryEntry[]>;
45
+ list(): Array<{collection:string;currentVersion:number;versions:number[]}>;
51
46
  }
52
-
53
- // ─── Reconcile ────────────────────────────────────────────────────────────────
54
-
55
- /** Result for one collection from a reconciliation scan. */
56
- export interface ReconcileCollectionResult {
57
- collection: string;
58
- /** Phase 1 stats: orphaned _mf_docmeta rows (crash between version-bump and insertOne). */
59
- phase1: { scanned: number; queued: number };
60
- /** Phase 2 stats: docs with no _mf_docmeta entry at all (crash before recordChange ran). */
61
- phase2: { scanned: number; queued: number };
62
- /** Total ops re-queued for this collection. */
63
- totalQueued: number;
64
- /** Whether local MongoDB is a replica set (Phase 1 is skipped on replica sets). */
65
- replicaSet: boolean;
66
- /** Set if this collection's reconcile threw an error. */
67
- error?: string;
47
+ export declare const schemaManager: SchemaManager;
48
+
49
+ // ─── Rate Limiter ─────────────────────────────────────────────────────────────
50
+ export interface TokenBucketOptions { capacity?: number; refillRate?: number; initialFill?: number; }
51
+ export interface CircuitBreakerOptions { failureThreshold?: number; successThreshold?: number; timeout?: number; halfOpenMax?: number; name?: string; }
52
+ export interface SyncRateLimiterOptions { tokenBucket?: TokenBucketOptions; circuitBreaker?: CircuitBreakerOptions; }
53
+ export interface CircuitBreakerStats { name: string; state: CircuitBreakerState; failureCount: number; successCount: number; lastFailureAt: number|null; }
54
+ export interface TokenBucketStats { available: number; capacity: number; refillRate: number; throttleCount: number; }
55
+ export interface SyncRateLimiterStats { tokenBucket: TokenBucketStats; circuitBreaker: CircuitBreakerStats; }
56
+ export declare class TokenBucket {
57
+ constructor(opts?: TokenBucketOptions);
58
+ consume(count?: number): boolean;
59
+ waitAndConsume(count?: number, timeoutMs?: number): Promise<boolean>;
60
+ waitTimeMs(count?: number): number;
61
+ readonly available: number;
68
62
  }
69
-
70
- export interface ReconcileResult {
71
- collections: ReconcileCollectionResult[];
72
- /** Total ops re-queued across all collections. */
73
- totalQueued: number;
63
+ export declare class CircuitBreaker {
64
+ constructor(opts?: CircuitBreakerOptions);
65
+ execute<T>(fn: ()=>Promise<T>): Promise<T>;
66
+ reset(): void;
67
+ on(event:'open'|'close'|'halfOpen', listener:(d:{prev:CircuitBreakerState;current:CircuitBreakerState})=>void): this;
68
+ stats(): CircuitBreakerStats;
69
+ readonly state: CircuitBreakerState;
70
+ readonly isOpen: boolean;
71
+ readonly isClosed: boolean;
74
72
  }
75
-
76
- // ─── Conflict record ──────────────────────────────────────────────────────────
77
-
78
- /** A changetrack record with ackState:'conflict' — unresolved sync conflict. */
79
- export interface ConflictRecord {
80
- _id: unknown;
81
- opId: string;
82
- type: 'create' | 'update' | 'delete';
83
- collection: string;
84
- docId: string;
85
- ownerKey: string;
86
- baseVersion: number;
87
- version: number;
88
- timestamp: Date;
89
- lastError?: string;
90
- retryCount: number;
73
+ export declare class CircuitOpenError extends Error { readonly circuitData: {state:CircuitBreakerState;retryAfterMs:number}; readonly retryAfterMs: number; }
74
+ export declare class SyncRateLimiter {
75
+ constructor(opts?: SyncRateLimiterOptions);
76
+ execute<T>(fn:()=>Promise<T>, tokenCost?: number, timeoutMs?: number): Promise<T>;
77
+ isBlocked(): boolean;
78
+ recordSuccess(): void;
79
+ recordFailure(err?: Error): void;
80
+ stats(): SyncRateLimiterStats;
81
+ reset(): void;
82
+ readonly circuitBreaker: CircuitBreaker;
83
+ readonly tokenBucket: TokenBucket;
91
84
  }
85
+ export declare class RateLimitError extends Error { readonly rateLimitData: {availableIn: number}; }
86
+ export declare function backoffDelay(attempt: number, baseDelayMs?: number, maxDelayMs?: number, strategy?: 'full'|'decorrelated'|'exponential'): number;
92
87
 
93
- // ─── Results ──────────────────────────────────────────────────────────────────
94
-
95
- export interface SyncResult {
96
- /** Docs downloaded from Atlas to local */
97
- downloaded: number;
98
- /** Docs uploaded from local to Atlas */
99
- uploaded: number;
100
- /** Delete operations applied */
101
- deleted: number;
102
- /** Operations that failed (network errors, etc.) */
103
- failed: number;
104
- /** Operations that produced a version conflict */
105
- conflicts: number;
106
- /** Upload attempts that were automatically retried */
107
- retried: number;
108
- /** Ops skipped via compression (offline create+delete, superseded updates) */
109
- skipped: number;
110
- }
88
+ // ─── Log Compactor ────────────────────────────────────────────────────────────
89
+ export interface CompactionOptions { conn?: unknown; retentionDays?: number; batchSize?: number; collection?: string; verbose?: boolean; }
90
+ export interface CompactionResult { scanned: number; deleted: number; kept: number; durationMs: number; }
91
+ export interface CompactionStats { totalSynced: number; oldSynced: number; retentionDays: number; cutoffDate: Date; collections: Array<{collection:string;count:number}>; estimatedDeletable: number; }
92
+ export declare function compactChangelog(opts?: CompactionOptions): Promise<CompactionResult>;
93
+ export declare function getCompactionStats(opts?: Omit<CompactionOptions,'batchSize'>): Promise<CompactionStats>;
111
94
 
112
- export interface SyncStatus {
113
- /** Whether Atlas is currently reachable */
114
- online: boolean;
115
- /** Total local ops not yet synced to Atlas */
116
- pending: number;
117
- /** Pending create operations */
118
- creates: number;
119
- /** Pending update operations */
120
- updates: number;
121
- /** Pending delete operations */
122
- deletes: number;
123
- /** Whether real-time change streams are active */
124
- realtime: boolean;
95
+ // ─── Configuration ────────────────────────────────────────────────────────────
96
+ export interface SyncConfig {
97
+ localUri?: string; atlasUri?: string; dbName?: string;
98
+ collections: string[];
99
+ syncInterval?: number; batchSize?: number;
100
+ syncOwner?: string | (()=>string);
101
+ realtime?: boolean;
102
+ onSync?: (result: SyncResult) => void;
103
+ onError?: (err: Error) => void;
104
+ cleanDays?: number;
105
+ reconcileOnStart?: boolean; reconcileFullScan?: boolean;
106
+ rateLimiter?: SyncRateLimiterOptions;
107
+ schemaManager?: SchemaManager;
108
+ /** Enable per-field LWW merge. Firebase can't do this. Default: true */
109
+ fieldLevelMerge?: boolean;
110
+ autoCompact?: boolean; compactEvery?: number; compactRetentionDays?: number;
111
+ compression?: boolean;
125
112
  }
126
113
 
127
- export interface OfflineResult {
128
- error: 'offline';
129
- pending: number;
130
- }
114
+ // ─── Results & Status ─────────────────────────────────────────────────────────
115
+ export interface SyncResult { downloaded: number; uploaded: number; deleted: number; failed: number; conflicts: number; retried: number; skipped: number; fieldMerged?: number; }
116
+ export interface SyncStatus { online: boolean; pending: number; creates: number; updates: number; deletes: number; realtime: boolean; circuitBreaker?: CircuitBreakerState; rateLimiter?: SyncRateLimiterStats; }
117
+ export interface OfflineResult { error: 'offline'; pending: number; }
118
+ export interface ConflictRecord { _id: unknown; opId: string; type: 'create'|'update'|'delete'; collection: string; docId: DocId; ownerKey: string; baseVersion: number; version: number; timestamp: Date; lastError?: string; retryCount: number; mergeAttempted?: boolean; }
119
+ export interface ReconcileCollectionResult { collection: string; phase1: {scanned:number;queued:number}; phase2: {scanned:number;queued:number}; totalQueued: number; replicaSet: boolean; error?: string; }
131
120
 
132
121
  // ─── Plugin ───────────────────────────────────────────────────────────────────
133
-
134
- export interface PluginOptions {
135
- /**
136
- * Dot-notation path to the field used as the owner key for multi-tenant
137
- * filtering. Example: 'userId' or 'org._id'.
138
- * Defaults to 'global' if not set.
139
- */
140
- ownerField?: string;
141
- /** Batch size for insertMany / updateMany / deleteMany hooks. Default: 200 */
142
- batchSize?: number;
143
- /** Concurrent recordChange calls per batch. Default: 4 */
144
- concurrency?: number;
145
- }
122
+ export interface PluginOptions { ownerField?: string; batchSize?: number; concurrency?: number; diffEnabled?: boolean; }
146
123
 
147
124
  // ─── Events ───────────────────────────────────────────────────────────────────
148
-
125
+ export interface ConflictData { collection: string; docId: DocId; opId: string; localVersion: number; remoteVersion: number; op: 'create'|'update'|'delete'; fieldMerge?: {attempted:boolean;resolvedFields:Record<string,'local'|'remote'>;conflictedFields:Record<string,{local:unknown;remote:unknown;winner:'local'|'remote'}>;score:number}; }
149
126
  export interface MongoFireEvents {
150
- /** Emitted once after start() completes successfully */
151
- ready: [];
152
- /** Emitted after each sync cycle with the result */
153
- sync: [result: SyncResult];
154
- /** Emitted when Atlas connection is (re)established */
155
- online: [];
156
- /** Emitted when Atlas becomes unreachable */
157
- offline: [];
158
- /** Emitted when real-time change streams activate */
159
- realtimeStarted: [];
160
- /** Emitted when real-time change streams are stopped */
161
- realtimeStopped: [];
162
- /** Emitted when stop() finishes */
163
- stopped: [];
164
- /** Emitted on unexpected sync errors */
165
- error: [err: Error];
166
127
  /**
167
- * Emitted when a version conflict is detected during upload.
168
- * The application should inspect conflictData and resolve manually.
128
+ * Fires as soon as the local MongoDB connection is live BEFORE Atlas
129
+ * connect, reconcile, or the sync timers start. Use this to gate
130
+ * `app.listen()` so the server never accepts requests before the DB is ready.
169
131
  */
132
+ localReady: [db: import('mongodb').Db];
133
+ ready: []; sync: [result: SyncResult]; online: []; offline: [];
134
+ realtimeStarted: []; realtimeStopped: []; stopped: []; error: [err: Error];
170
135
  conflict: [data: ConflictData];
136
+ fieldMerged: [data: {collection:string;docId:DocId;opId:string;resolvedFields:Record<string,'local'|'remote'>;score:number}];
137
+ reconcileComplete: [result: {collections:ReconcileCollectionResult[];totalQueued:number}];
138
+ conflictResolved: [data: {opId:string;resolution:'retried'|'dismissed'}];
139
+ compacted: [result: CompactionResult];
140
+ circuitOpen: [data: {retryAfterMs:number}]; circuitClose: [];
141
+ migrated: [data: {collection:string;docId:DocId;fromVersion:number;toVersion:number}];
142
+ }
143
+
144
+ // ─── Internal document structure ──────────────────────────────────────────────
145
+ export interface MfDocFields { _mf_version: number; _mf_lastWriterDeviceId: string; _mf_opId: string; _mf_lastOpId: string; _mf_schemaVersion?: number; }
146
+ export interface ChangetrackRecord extends MfDocFields { _id: unknown; opId: string; type: 'create'|'update'|'delete'; collection: string; docId: DocId; payload: Record<string,unknown>|PatchPayload|null; ownerKey: string; deviceId: string; timestamp: Date; updatedAt: Date; baseVersion: number; version: number; lastWriterDeviceId: string; payloadChecksum: string|null; synced: boolean; ackState: 'pending'|'acked'|'verified'|'conflict'|'error'|'compressed'|'superseded'|'dismissed'; retryCount: number; createdAt: Date; syncedAt: Date|null; remoteAckAt: Date|null; checksumVerifiedAt: Date|null; lastError: string|null; applyStatus?: 'pending'|'applied'|'conflict'; }
147
+ export interface DocMetaRecord { _id: string; collection: string; docId: DocId; version: number; updatedAt: Date; lastWriterDeviceId: string; deleted: boolean; ownerKey: string; updatedByOpId: string|null; lastOpId: string|null; payloadChecksum: string|null; createdAt: Date; }
148
+
149
+ // ─── LocalManager ─────────────────────────────────────────────────────────────
150
+ /**
151
+ * Auto-manages the Mongoose connection to local MongoDB.
152
+ * Spawns `mongod` via the system binary if MongoDB is not running,
153
+ * and retries until the connection succeeds.
154
+ *
155
+ * **Developers should never call `mongoose.connect()` directly.**
156
+ * MongoFire calls `ensureConnected()` internally during `start()`.
157
+ */
158
+ export declare class LocalManager {
171
159
  /**
172
- * Emitted after a reconciliation scan completes (startup or manual).
173
- * totalQueued > 0 means previously-lost writes were recovered and will sync
174
- * on the next upload cycle.
160
+ * Ensure Mongoose is connected to local MongoDB.
161
+ * Idempotent returns immediately if already connected.
162
+ * Uses `process.env.LOCAL_URI` / `process.env.DB_NAME`, falling back to
163
+ * the supplied values when the env vars are absent.
175
164
  */
176
- reconcileComplete: [result: ReconcileResult];
165
+ ensureConnected(fallbackUri?: string, fallbackDbName?: string): Promise<void>;
177
166
  /**
178
- * Emitted when SyncConfig.reconcileOnStart is true and conflicts are detected
179
- * after the reconciliation scan finishes. Also emitted when clean() removes
180
- * stale conflict records.
167
+ * Disconnect Mongoose and stop the spawned `mongod` process (if any).
168
+ * Called automatically by `mongofire.stop()`.
181
169
  */
182
- conflictResolved: [data: { opId: string; resolution: 'retried' | 'dismissed' }];
183
- }
184
-
185
- /** Structured data emitted with the 'conflict' event. */
186
- export interface ConflictData {
187
- /** Collection where the conflict occurred */
188
- collection: string;
189
- /** Document _id (as string) */
190
- docId: string;
191
- /** The opId of the conflicting local operation */
192
- opId: string;
193
- /** The local baseVersion that was expected on Atlas */
194
- localVersion: number;
195
- /** The actual version currently on Atlas */
196
- remoteVersion: number;
197
- /** Operation type: 'create' | 'update' | 'delete' */
198
- op: string;
170
+ closeAll(): Promise<void>;
171
+ readonly isConnected: boolean;
172
+ readonly mongodProcess: import('child_process').ChildProcess | null;
199
173
  }
174
+ /** Singleton LocalManager shared across all MongoFire instances in the process. */
175
+ export declare const localManager: LocalManager;
200
176
 
201
177
  // ─── Main class ───────────────────────────────────────────────────────────────
202
-
203
178
  export declare class MongoFire extends EventEmitter {
204
179
  /**
205
- * Connect to local and Atlas MongoDB, run an initial sync, and start
206
- * background polling. Concurrent calls share one init promise.
180
+ * Start MongoFire with the given config. Connects to local MongoDB first,
181
+ * then Atlas. The returned Promise resolves when Atlas is connected and the
182
+ * initial sync is complete (or when Atlas is determined to be offline).
183
+ *
184
+ * **Do not call `mongoose.connect()` manually** — MongoFire manages this.
185
+ * Use `localReady` / `waitForLocal()` to gate `app.listen()` instead.
207
186
  */
208
187
  start(config: SyncConfig): Promise<this>;
209
188
 
210
189
  /**
211
- * Flush pending ops, wait for any active sync to finish, and close all
212
- * connections.
213
- * @param timeoutMs Max ms to wait for active sync. Default: 10000
190
+ * Returns a Promise that resolves with the native `Db` handle as soon as the
191
+ * LOCAL MongoDB connection is ready — before Atlas connect, reconcile, or
192
+ * the sync timer start.
193
+ *
194
+ * Use this to gate `app.listen()` so incoming requests never arrive before
195
+ * the local database is available:
196
+ *
197
+ * ```js
198
+ * const { localReady } = require('./mongofire');
199
+ * localReady.then(() => app.listen(3000));
200
+ * // or: await mongofire.waitForLocal(); app.listen(3000);
201
+ * ```
202
+ *
203
+ * If `start()` has not been called yet (and no `mongofire.config.js` was
204
+ * found for auto-start), this Promise rejects after 60 s with a clear
205
+ * "did you forget to call start()?" message.
214
206
  */
215
- stop(timeoutMs?: number): Promise<void>;
207
+ waitForLocal(): Promise<import('mongodb').Db>;
216
208
 
217
209
  /**
218
- * Manually trigger a sync cycle.
219
- * Returns an OfflineResult immediately if Atlas is unreachable.
210
+ * Convenience alias for `waitForLocal()` — a Promise getter so you can
211
+ * destructure it at module level without calling a function:
212
+ *
213
+ * ```js
214
+ * const { localReady } = require('./mongofire');
215
+ * localReady.then(() => app.listen(3000));
216
+ * ```
220
217
  */
221
- sync(type?: 'required' | 'all'): Promise<SyncResult | OfflineResult>;
222
-
223
- /** Returns pending op counts and online status. */
224
- status(): Promise<SyncStatus>;
218
+ readonly localReady: Promise<import('mongodb').Db>;
225
219
 
226
220
  /**
227
- * Delete old synced and stale conflict changetrack records.
221
+ * Automatically loads `mongofire.config.js` (or `.cjs` / `.mjs`) from
222
+ * `process.cwd()` and calls `start(config)`. Called internally when the
223
+ * package is required outside the CLI context so that users do not need a
224
+ * separate `mongofire.js` entry file.
228
225
  *
229
- * CRIT-6 FIX: The original clean() only removed records with synced:true.
230
- * Conflict records (ackState:'conflict', synced:false) were never touched
231
- * and grew indefinitely. This version also cleans stale conflict records.
232
- *
233
- * @param days Delete synced records older than this many days. Default: 7
234
- * @param opts.conflictDays Delete conflict records older than this many days.
235
- * Default: same as `days`.
236
- * @returns Total number of records deleted
226
+ * Safe to call manually; `start()` is idempotent.
237
227
  */
238
- clean(days?: number, opts?: { conflictDays?: number }): Promise<number>;
228
+ autoStart(): Promise<this>;
239
229
 
240
230
  /**
241
- * Returns all unresolved conflict records, optionally filtered to one collection.
231
+ * **Primary developer API** replaces `app.listen()`.
232
+ *
233
+ * Waits for local MongoDB to be ready, then starts the Express (or any
234
+ * Node `http.Server`) application on the given port. If the local DB
235
+ * fails to connect, logs a descriptive error and calls `process.exit(1)`
236
+ * so the server never starts in a broken state.
237
+ *
238
+ * ```js
239
+ * // server.js
240
+ * import { startApp } from './mongofire.js';
241
+ * startApp(app, process.env.PORT || 3000);
242
+ * ```
242
243
  *
243
- * @example
244
- * const conflicts = await mongofire.conflicts();
245
- * for (const c of conflicts) {
246
- * console.log(`Conflict in ${c.collection}/${c.docId} — local v${c.version}, error: ${c.lastError}`);
247
- * await mongofire.retryConflict(c.opId); // retry upload
248
- * // OR
249
- * await mongofire.dismissConflict(c.opId); // accept local state, discard record
250
- * }
244
+ * @param app Any object with a `.listen(port, callback)` method (Express, Fastify, etc.)
245
+ * @param port TCP port to listen on (default: 3000)
246
+ * @returns Promise resolving to the `http.Server` returned by `app.listen()`
251
247
  */
252
- conflicts(collection?: string): Promise<ConflictRecord[]>;
248
+ startApp(app: { listen: (port: number, cb?: (err?: Error) => void) => import('http').Server }, port?: number | string): Promise<import('http').Server>;
253
249
 
254
- /**
255
- * Reset a conflict record back to 'pending' so the next sync cycle retries
256
- * the upload. Use this after manually resolving the remote state on Atlas.
257
- */
250
+ stop(timeoutMs?: number): Promise<void>;
251
+ sync(type?: 'required'|'all'): Promise<SyncResult|OfflineResult>;
252
+ status(): Promise<SyncStatus>;
253
+ clean(days?: number, opts?: {conflictDays?:number}): Promise<number>;
254
+ conflicts(collection?: string): Promise<ConflictRecord[]>;
258
255
  retryConflict(opId: string): Promise<void>;
259
-
260
- /**
261
- * Dismiss a conflict record — marks it as resolved without retrying the upload.
262
- * Use this when the local write should be silently discarded (remote wins).
263
- * The record is marked synced:true so the TTL index will clean it up.
264
- */
265
256
  dismissConflict(opId: string): Promise<void>;
266
-
267
257
  /**
268
- * Returns a Mongoose schema plugin that tracks changes on the given collection.
269
- *
270
- * @example
271
- * import mongofire from 'mongofire';
272
- * userSchema.plugin(mongofire.plugin('users', { ownerField: 'userId' }));
258
+ * Drops all MongoFire internal collections and the configured data
259
+ * collections so the next `start()` performs a clean bootstrap from Atlas.
260
+ * **Any unsynced local changes are permanently lost.**
273
261
  */
262
+ resetLocal(): Promise<{ dropped: number }>;
274
263
  plugin(collectionName: string, options?: PluginOptions): (schema: import('mongoose').Schema) => void;
275
-
276
- /** Manually activate real-time sync (if Atlas is connected and supports change streams). */
277
264
  startRealtime(): Promise<boolean>;
278
-
279
- /** Stop real-time change stream (polling continues). */
280
265
  stopRealtime(): Promise<void>;
281
-
282
- /**
283
- * Run a reconciliation scan to recover any writes that were lost due to a
284
- * crash between the Mongoose data write and the changetrack insertOne.
285
- *
286
- * Also runs automatically at startup (controlled by `reconcileOnStart` config).
287
- *
288
- * @param collectionOrOpts A single collection name, an array of names, or
289
- * an options object to scan all configured collections.
290
- * @param opts { fullScan?: boolean; verbose?: boolean }
291
- *
292
- * @example
293
- * // Scan all collections
294
- * const results = await mongofire.reconcile();
295
- *
296
- * @example
297
- * // Scan one collection, Phase 1 only (fast)
298
- * await mongofire.reconcile('orders', { fullScan: false });
299
- *
300
- * @example
301
- * // Listen for recovery events
302
- * mongofire.on('reconcileComplete', ({ totalQueued }) => {
303
- * if (totalQueued > 0) console.warn(`Recovered ${totalQueued} lost writes`);
304
- * });
305
- */
306
- reconcile(
307
- collectionOrOpts?: string | string[] | { fullScan?: boolean; verbose?: boolean },
308
- opts?: { fullScan?: boolean; verbose?: boolean }
309
- ): Promise<ReconcileCollectionResult[]>;
310
-
311
- /** Whether Atlas is currently reachable. */
266
+ reconcile(collectionOrOpts?: string|string[]|{fullScan?:boolean;verbose?:boolean}, opts?: {fullScan?:boolean;verbose?:boolean}): Promise<ReconcileCollectionResult[]>;
267
+ compact(opts?: CompactionOptions): Promise<CompactionResult>;
268
+ rateLimiterStats(): SyncRateLimiterStats | null;
269
+ resetCircuit(): void;
312
270
  readonly online: boolean;
313
- /** Whether start() has completed. */
314
271
  readonly started: boolean;
315
- /** Whether a sync cycle is currently running. */
316
272
  readonly syncing: boolean;
317
- /** Whether real-time change streams are active. */
318
273
  readonly realtimeActive: boolean;
319
-
320
- // ─── Typed event emitter overloads ────────────────────────────────────────
274
+ readonly schemaManager: SchemaManager | null;
275
+ readonly rateLimiter: SyncRateLimiter | null;
321
276
  on<K extends keyof MongoFireEvents>(event: K, listener: (...args: MongoFireEvents[K]) => void): this;
322
277
  once<K extends keyof MongoFireEvents>(event: K, listener: (...args: MongoFireEvents[K]) => void): this;
323
278
  emit<K extends keyof MongoFireEvents>(event: K, ...args: MongoFireEvents[K]): boolean;
@@ -325,27 +280,38 @@ export declare class MongoFire extends EventEmitter {
325
280
  }
326
281
 
327
282
  // ─── Direct plugin import ─────────────────────────────────────────────────────
283
+ export declare function mongofirePlugin(schema: import('mongoose').Schema, options?: PluginOptions & {collection?: string}): void;
284
+ export declare namespace mongofirePlugin { function factory(collectionName: string, options?: PluginOptions): (schema: import('mongoose').Schema) => void; }
328
285
 
286
+ // ─── Standalone named exports (destructurable from the package / wrapper) ─────
329
287
  /**
330
- * Raw Mongoose plugin factory for direct schema.plugin() usage.
331
- * Equivalent to mongofire.plugin(name, opts) but requires `collection` in options.
288
+ * Starts the Express app after waiting for local MongoDB.
289
+ * Exits the process if the DB is unavailable.
332
290
  *
333
- * @example
334
- * import mongofirePlugin from 'mongofire/plugin';
335
- * userSchema.plugin(mongofirePlugin, { collection: 'users', ownerField: 'userId' });
291
+ * ```ts
292
+ * import { startApp } from './mongofire.js';
293
+ * startApp(app, 3000);
294
+ * ```
295
+ */
296
+ export declare function startApp(
297
+ app: { listen: (port: number, cb?: (err?: Error) => void) => import('http').Server },
298
+ port?: number | string
299
+ ): Promise<import('http').Server>;
300
+
301
+ /**
302
+ * Attaches MongoFire change-tracking to a Mongoose schema.
336
303
  *
337
- * @example
338
- * // Or use the factory helper (matches instance.plugin() return shape):
339
- * import { factory } from 'mongofire/plugin';
340
- * userSchema.plugin(factory('users', { ownerField: 'userId' }));
304
+ * ```ts
305
+ * import { plugin } from './mongofire.js';
306
+ * UserSchema.plugin(plugin('users'));
307
+ * StudentSchema.plugin(plugin('students', { ownerField: 'userId' }));
308
+ * ```
341
309
  */
342
- export declare function mongofirePlugin(schema: import('mongoose').Schema, options?: PluginOptions & { collection?: string }): void;
343
- export declare namespace mongofirePlugin {
344
- function factory(collectionName: string, options?: PluginOptions): (schema: import('mongoose').Schema) => void;
345
- }
310
+ export declare function plugin(
311
+ collectionName: string,
312
+ options?: PluginOptions
313
+ ): (schema: import('mongoose').Schema) => void;
346
314
 
347
315
  // ─── Default export ───────────────────────────────────────────────────────────
348
-
349
- /** The default MongoFire singleton instance. */
350
- declare const mongofire: MongoFire & { MongoFire: typeof MongoFire };
316
+ declare const mongofire: MongoFire & { MongoFire: typeof MongoFire; startApp: typeof startApp; plugin: typeof plugin; };
351
317
  export default mongofire;
package/index.cjs CHANGED
@@ -1,4 +1,18 @@
1
1
  'use strict';
2
- // Entry point obfuscated dist/ (built by: npm run build)
3
- // Never edit this file — edit src/index.cjs instead
4
- module.exports = require('./dist/src/index.cjs');
2
+ // index.cjs MongoFire entry point
3
+ //
4
+ // Resolution order:
5
+ // 1. ./dist/src/index.cjs — built production bundle (created by `npm run build`)
6
+ // 2. ./src/index.cjs — live source (fallback for local dev / npm link)
7
+ //
8
+ // ⚠️ If you see errors referencing dist/src/index.cjs after updating the
9
+ // source, run `npm run build` (or delete the dist/ folder) to clear the
10
+ // stale build. The dist/ folder is NOT included in the git repo and must
11
+ // be regenerated after every source change.
12
+ let _mod;
13
+ try {
14
+ _mod = require('./dist/src/index.cjs');
15
+ } catch (_) {
16
+ _mod = require('./src/index.cjs');
17
+ }
18
+ module.exports = _mod;