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.
- package/CHANGELOG.md +35 -67
- package/README.md +338 -155
- package/bin/mongofire.cjs +1039 -654
- package/dist/bin/mongofire.cjs +1 -851
- package/dist/src/changetrack.js +1 -1
- package/dist/src/compactor.js +1 -0
- package/dist/src/connection.js +1 -1
- package/dist/src/device.js +1 -1
- package/dist/src/diff.js +1 -0
- package/dist/src/field-merge.js +1 -0
- package/dist/src/index.cjs +1 -1
- package/dist/src/local-manager.js +1 -0
- package/dist/src/plugin.js +1 -1
- package/dist/src/plugin.mjs +6 -0
- package/dist/src/rate-limiter.js +1 -0
- package/dist/src/reconcile.js +1 -1
- package/dist/src/schema-manager.js +1 -0
- package/dist/src/state.js +1 -1
- package/dist/src/sync.js +1 -1
- package/dist/src/utils.js +1 -1
- package/dist/types/index.d.ts +247 -281
- package/index.cjs +17 -3
- package/index.mjs +7 -2
- package/package.json +18 -16
- package/types/index.d.ts +317 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -1,323 +1,278 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
|
|
3
|
-
// ───
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
export interface
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
// ───
|
|
94
|
-
|
|
95
|
-
export interface
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
*
|
|
168
|
-
*
|
|
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
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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
|
-
|
|
165
|
+
ensureConnected(fallbackUri?: string, fallbackDbName?: string): Promise<void>;
|
|
177
166
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* stale conflict records.
|
|
167
|
+
* Disconnect Mongoose and stop the spawned `mongod` process (if any).
|
|
168
|
+
* Called automatically by `mongofire.stop()`.
|
|
181
169
|
*/
|
|
182
|
-
|
|
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
|
-
*
|
|
206
|
-
*
|
|
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
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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
|
-
|
|
207
|
+
waitForLocal(): Promise<import('mongodb').Db>;
|
|
216
208
|
|
|
217
209
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
228
|
+
autoStart(): Promise<this>;
|
|
239
229
|
|
|
240
230
|
/**
|
|
241
|
-
*
|
|
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
|
-
* @
|
|
244
|
-
*
|
|
245
|
-
*
|
|
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
|
-
|
|
248
|
+
startApp(app: { listen: (port: number, cb?: (err?: Error) => void) => import('http').Server }, port?: number | string): Promise<import('http').Server>;
|
|
253
249
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
331
|
-
*
|
|
288
|
+
* Starts the Express app after waiting for local MongoDB.
|
|
289
|
+
* Exits the process if the DB is unavailable.
|
|
332
290
|
*
|
|
333
|
-
*
|
|
334
|
-
* import
|
|
335
|
-
*
|
|
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
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
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;
|