@semics-tech/mongolite 1.3.0 → 1.4.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/README.md +80 -21
- package/dist/cjs/collection.js +9 -6
- package/dist/cjs/cursors/findCursor.js +6 -4
- package/dist/cjs/index.js +108 -1
- package/dist/cjs/mongo-client.js +18 -1
- package/dist/cjs/server.js +164 -0
- package/dist/cjs/sync/commands.js +126 -0
- package/dist/cjs/sync/diff.js +174 -0
- package/dist/cjs/sync/httpSink.js +159 -0
- package/dist/cjs/sync/index.js +40 -0
- package/dist/cjs/sync/mongoExecutor.js +306 -0
- package/dist/cjs/sync/mongoSink.js +161 -0
- package/dist/cjs/sync/outbox.js +358 -0
- package/dist/cjs/sync/protocol.js +177 -0
- package/dist/cjs/sync/replicator.js +747 -0
- package/dist/cjs/sync/shadow.js +178 -0
- package/dist/cjs/sync/types.js +2 -0
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js.map +1 -1
- package/dist/collection.js +9 -6
- package/dist/collection.js.map +1 -1
- package/dist/cursors/findCursor.d.ts +2 -1
- package/dist/cursors/findCursor.js +6 -4
- package/dist/cursors/findCursor.js.map +1 -1
- package/dist/index.d.ts +68 -2
- package/dist/index.js +96 -1
- package/dist/index.js.map +1 -1
- package/dist/mongo-client.d.ts +13 -0
- package/dist/mongo-client.js +18 -1
- package/dist/mongo-client.js.map +1 -1
- package/dist/server.d.ts +97 -0
- package/dist/server.js +158 -0
- package/dist/server.js.map +1 -0
- package/dist/sync/commands.d.ts +49 -0
- package/dist/sync/commands.js +118 -0
- package/dist/sync/commands.js.map +1 -0
- package/dist/sync/diff.d.ts +55 -0
- package/dist/sync/diff.js +169 -0
- package/dist/sync/diff.js.map +1 -0
- package/dist/sync/httpSink.d.ts +93 -0
- package/dist/sync/httpSink.js +155 -0
- package/dist/sync/httpSink.js.map +1 -0
- package/dist/sync/index.d.ts +17 -0
- package/dist/sync/index.js +10 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/mongoExecutor.d.ts +37 -0
- package/dist/sync/mongoExecutor.js +302 -0
- package/dist/sync/mongoExecutor.js.map +1 -0
- package/dist/sync/mongoSink.d.ts +109 -0
- package/dist/sync/mongoSink.js +123 -0
- package/dist/sync/mongoSink.js.map +1 -0
- package/dist/sync/outbox.d.ts +120 -0
- package/dist/sync/outbox.js +355 -0
- package/dist/sync/outbox.js.map +1 -0
- package/dist/sync/protocol.d.ts +93 -0
- package/dist/sync/protocol.js +168 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/replicator.d.ts +177 -0
- package/dist/sync/replicator.js +744 -0
- package/dist/sync/replicator.js.map +1 -0
- package/dist/sync/shadow.d.ts +54 -0
- package/dist/sync/shadow.js +173 -0
- package/dist/sync/shadow.js.map +1 -0
- package/dist/sync/types.d.ts +388 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- package/package.json +16 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { SyncApplyResult, SyncIdMapping, SyncOperation, SyncSink } from './types.js';
|
|
2
|
+
/** The subset of the global `fetch` signature this sink relies on. */
|
|
3
|
+
export type FetchLike = (url: string, init: {
|
|
4
|
+
method: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
body: string;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}) => Promise<{
|
|
9
|
+
ok: boolean;
|
|
10
|
+
status: number;
|
|
11
|
+
headers: {
|
|
12
|
+
get(name: string): string | null;
|
|
13
|
+
};
|
|
14
|
+
text(): Promise<string>;
|
|
15
|
+
}>;
|
|
16
|
+
export interface HttpSinkOptions {
|
|
17
|
+
/** Base URL of the remote API, e.g. `https://api.example.com`. */
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
/** Target database name, included in the request path. */
|
|
20
|
+
database: string;
|
|
21
|
+
/** Path segment before the database. Defaults to `'sync'`. */
|
|
22
|
+
pathPrefix?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Headers to attach to every request, resolved per request so short-lived tokens can
|
|
25
|
+
* be refreshed. Anything static belongs in {@link HttpSinkOptions.headers}.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* getAuthHeaders: async () => ({ Authorization: `Bearer ${await getAccessToken()}` })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
getAuthHeaders?: () => Promise<Record<string, string>> | Record<string, string>;
|
|
33
|
+
/** Static headers sent with every request. */
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Capture and replay `set-cookie`, for APIs that use a session cookie for affinity
|
|
37
|
+
* after the first authenticated request. Defaults to `true`.
|
|
38
|
+
*/
|
|
39
|
+
cookies?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* `fetch` implementation. Defaults to the global one (Node 18+).
|
|
42
|
+
*
|
|
43
|
+
* Supply your own to get interceptors, connection pooling or proxy support — for
|
|
44
|
+
* example `ofetch`. Note that retries are handled by the replicator, so a fetch layer
|
|
45
|
+
* configured to retry will compound with that rather than replace it.
|
|
46
|
+
*/
|
|
47
|
+
fetch?: FetchLike;
|
|
48
|
+
/** Per-request timeout in ms. Defaults to `30_000`. */
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
/** See {@link SyncIdMapping}. Defaults to `'auto'`. */
|
|
51
|
+
idMapping?: SyncIdMapping;
|
|
52
|
+
/** Conditional writes against `_v`. Defaults to `true`. */
|
|
53
|
+
versioning?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Include the built MongoDB command alongside each operation. Defaults to `true`,
|
|
56
|
+
* so a forwarding API can pass it straight through. Turn it off to halve payload
|
|
57
|
+
* size when the receiver rebuilds commands anyway (which it does by default).
|
|
58
|
+
*/
|
|
59
|
+
includeCommands?: boolean;
|
|
60
|
+
/** Replicator name, sent for server-side logging. */
|
|
61
|
+
replicator?: string;
|
|
62
|
+
}
|
|
63
|
+
export declare class HttpUpstreamSink implements SyncSink {
|
|
64
|
+
private readonly options;
|
|
65
|
+
readonly name = "http";
|
|
66
|
+
private readonly fetchImpl;
|
|
67
|
+
private readonly idMapping;
|
|
68
|
+
private readonly versioning;
|
|
69
|
+
private cookie;
|
|
70
|
+
constructor(options: HttpSinkOptions);
|
|
71
|
+
apply(operations: SyncOperation[]): Promise<SyncApplyResult>;
|
|
72
|
+
fetch(collection: string, documentIds: string[]): Promise<Record<string, unknown>[]>;
|
|
73
|
+
private endpoint;
|
|
74
|
+
private requestHeaders;
|
|
75
|
+
/**
|
|
76
|
+
* Posts a body and decodes the reply.
|
|
77
|
+
*
|
|
78
|
+
* Retries are deliberately *not* handled here — the replicator already owns backoff,
|
|
79
|
+
* outage buffering and checkpointing. This throws on anything retryable and lets it do
|
|
80
|
+
* its job; throwing twice over would compound the delays.
|
|
81
|
+
*/
|
|
82
|
+
private send;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The remote API refused the request outright.
|
|
86
|
+
*
|
|
87
|
+
* Distinct from a transport failure: retrying will produce the same refusal, so the
|
|
88
|
+
* batch is dead-lettered rather than retried.
|
|
89
|
+
*/
|
|
90
|
+
export declare class SyncRequestRejected extends Error {
|
|
91
|
+
readonly status: number;
|
|
92
|
+
constructor(message: string, status: number);
|
|
93
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A {@link SyncSink} that ships changes to a remote HTTP API instead of connecting to
|
|
3
|
+
* MongoDB directly, for deployments where the local instance cannot reach the database.
|
|
4
|
+
*
|
|
5
|
+
* The API receives MongoDB-shaped commands and applies them on the client's behalf —
|
|
6
|
+
* see `@semics-tech/mongolite/server` for a receiver that does exactly that.
|
|
7
|
+
*
|
|
8
|
+
* Version predicates travel with each operation and conflicts come back in the response,
|
|
9
|
+
* so replication behaves the same over HTTP as it does over a direct connection: the
|
|
10
|
+
* upstream stays the source of truth, and a concurrent writer is detected rather than
|
|
11
|
+
* overwritten.
|
|
12
|
+
*/
|
|
13
|
+
import { buildWriteCommand } from './commands.js';
|
|
14
|
+
import { SYNC_PROTOCOL_VERSION, SYNC_CONTENT_TYPE, decodeBody, encodeBody, toWireOperation, } from './protocol.js';
|
|
15
|
+
export class HttpUpstreamSink {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.name = 'http';
|
|
19
|
+
this.cookie = null;
|
|
20
|
+
if (!options.baseUrl)
|
|
21
|
+
throw new Error('HttpUpstreamSink requires a `baseUrl`.');
|
|
22
|
+
if (!options.database)
|
|
23
|
+
throw new Error('HttpUpstreamSink requires a `database`.');
|
|
24
|
+
const globalFetch = globalThis.fetch;
|
|
25
|
+
if (!options.fetch && typeof globalFetch !== 'function') {
|
|
26
|
+
throw new Error('HttpUpstreamSink needs a `fetch` implementation. Node 18+ provides one globally; ' +
|
|
27
|
+
'on older runtimes pass `fetch` explicitly.');
|
|
28
|
+
}
|
|
29
|
+
this.fetchImpl = options.fetch ?? globalFetch;
|
|
30
|
+
this.idMapping = options.idMapping ?? 'auto';
|
|
31
|
+
this.versioning = options.versioning ?? true;
|
|
32
|
+
}
|
|
33
|
+
async apply(operations) {
|
|
34
|
+
const includeCommands = this.options.includeCommands ?? true;
|
|
35
|
+
const request = {
|
|
36
|
+
protocol: SYNC_PROTOCOL_VERSION,
|
|
37
|
+
replicator: this.options.replicator,
|
|
38
|
+
operations: operations.map((op) => toWireOperation(op, includeCommands
|
|
39
|
+
? buildWriteCommand(op, { idMapping: this.idMapping, versioning: this.versioning })
|
|
40
|
+
: undefined)),
|
|
41
|
+
};
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await this.send('_sync', request);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
// A request the API rejects outright will be rejected again on every retry, so
|
|
48
|
+
// it is reported as a per-operation failure — dead-lettered, not spun on. The
|
|
49
|
+
// sink's contract is that throwing means "transient", and this is not that.
|
|
50
|
+
if (err instanceof SyncRequestRejected) {
|
|
51
|
+
return {
|
|
52
|
+
applied: 0,
|
|
53
|
+
failures: operations.map((_op, index) => ({
|
|
54
|
+
index,
|
|
55
|
+
message: err.message,
|
|
56
|
+
code: err.status,
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
applied: response.applied ?? 0,
|
|
64
|
+
failures: response.failures,
|
|
65
|
+
conflicts: response.conflicts,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async fetch(collection, documentIds) {
|
|
69
|
+
if (documentIds.length === 0)
|
|
70
|
+
return [];
|
|
71
|
+
const response = await this.send('_sync/fetch', {
|
|
72
|
+
protocol: SYNC_PROTOCOL_VERSION,
|
|
73
|
+
collection,
|
|
74
|
+
documentIds,
|
|
75
|
+
});
|
|
76
|
+
return response.documents ?? [];
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------- internals
|
|
79
|
+
endpoint(path) {
|
|
80
|
+
const base = this.options.baseUrl.replace(/\/+$/, '');
|
|
81
|
+
const prefix = (this.options.pathPrefix ?? 'sync').replace(/^\/+|\/+$/g, '');
|
|
82
|
+
return `${base}/${prefix}/${encodeURIComponent(this.options.database)}/${path}`;
|
|
83
|
+
}
|
|
84
|
+
async requestHeaders() {
|
|
85
|
+
const headers = {
|
|
86
|
+
'content-type': SYNC_CONTENT_TYPE,
|
|
87
|
+
accept: SYNC_CONTENT_TYPE,
|
|
88
|
+
...this.options.headers,
|
|
89
|
+
...(this.options.getAuthHeaders ? await this.options.getAuthHeaders() : {}),
|
|
90
|
+
};
|
|
91
|
+
if (this.cookie && (this.options.cookies ?? true))
|
|
92
|
+
headers.cookie = this.cookie;
|
|
93
|
+
return headers;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Posts a body and decodes the reply.
|
|
97
|
+
*
|
|
98
|
+
* Retries are deliberately *not* handled here — the replicator already owns backoff,
|
|
99
|
+
* outage buffering and checkpointing. This throws on anything retryable and lets it do
|
|
100
|
+
* its job; throwing twice over would compound the delays.
|
|
101
|
+
*/
|
|
102
|
+
async send(path, body) {
|
|
103
|
+
const timeoutMs = this.options.timeoutMs ?? 30000;
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await this.fetchImpl(this.endpoint(path), {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: await this.requestHeaders(),
|
|
111
|
+
body: encodeBody(body),
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
// Network failure, DNS, timeout — all transient as far as we can tell.
|
|
117
|
+
throw new Error(`Sync request to ${this.endpoint(path)} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
if (this.options.cookies ?? true) {
|
|
123
|
+
const setCookie = response.headers.get('set-cookie');
|
|
124
|
+
if (setCookie)
|
|
125
|
+
this.cookie = setCookie;
|
|
126
|
+
}
|
|
127
|
+
const text = await response.text();
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const detail = text.slice(0, 500);
|
|
130
|
+
// 4xx means the request itself is wrong and will stay wrong; throwing would spin
|
|
131
|
+
// forever. Surfacing it as a per-operation failure dead-letters the batch instead.
|
|
132
|
+
// 408 and 429 are the exceptions — they explicitly invite a retry.
|
|
133
|
+
const retryable = response.status >= 500 || response.status === 408 || response.status === 429;
|
|
134
|
+
if (retryable) {
|
|
135
|
+
throw new Error(`Sync request failed with ${response.status}: ${detail}`);
|
|
136
|
+
}
|
|
137
|
+
throw new SyncRequestRejected(`Sync request rejected with ${response.status}: ${detail}`, response.status);
|
|
138
|
+
}
|
|
139
|
+
return decodeBody(text);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The remote API refused the request outright.
|
|
144
|
+
*
|
|
145
|
+
* Distinct from a transport failure: retrying will produce the same refusal, so the
|
|
146
|
+
* batch is dead-lettered rather than retried.
|
|
147
|
+
*/
|
|
148
|
+
export class SyncRequestRejected extends Error {
|
|
149
|
+
constructor(message, status) {
|
|
150
|
+
super(message);
|
|
151
|
+
this.status = status;
|
|
152
|
+
this.name = 'SyncRequestRejected';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=httpSink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpSink.js","sourceRoot":"","sources":["../../src/sync/httpSink.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { SyncReplicator } from './replicator.js';
|
|
2
|
+
export { SyncOutbox, OUTBOX_TABLE, STATE_TABLE, DEAD_LETTER_TABLE, SYNC_OBJECT_PREFIX, } from './outbox.js';
|
|
3
|
+
export { MongoUpstreamSink, VERSION_FIELD, UPDATED_AT_FIELD } from './mongoSink.js';
|
|
4
|
+
export type { MongoSinkOptions, MongoUpstreamAuth } from './mongoSink.js';
|
|
5
|
+
export { HttpUpstreamSink, SyncRequestRejected } from './httpSink.js';
|
|
6
|
+
export type { HttpSinkOptions, FetchLike } from './httpSink.js';
|
|
7
|
+
export { buildWriteCommand, toUpstreamId } from './commands.js';
|
|
8
|
+
export type { MongoWriteCommand, BuildCommandOptions, ObjectIdFactory } from './commands.js';
|
|
9
|
+
export { executeWrites, fetchDocuments } from './mongoExecutor.js';
|
|
10
|
+
export type { PreparedWrite, ExecuteOptions } from './mongoExecutor.js';
|
|
11
|
+
export { SYNC_PROTOCOL_VERSION, SyncProtocolError, encodeBody, decodeBody, toWireOperation, fromWireOperation, } from './protocol.js';
|
|
12
|
+
export type { SyncApplyRequest, SyncApplyResponse, SyncFetchRequest, SyncFetchResponse, WireOperation, } from './protocol.js';
|
|
13
|
+
export { SyncShadow, SHADOW_TABLE, projectToLocalShape } from './shadow.js';
|
|
14
|
+
export type { ShadowEntry } from './shadow.js';
|
|
15
|
+
export { computeDiff, applyDiff, isEmptyDiff, deepEqual } from './diff.js';
|
|
16
|
+
export type { DocumentDiff } from './diff.js';
|
|
17
|
+
export type { MongoBulkWriteResultLike, SyncApplyConflict, SyncApplyFailure, SyncApplyResult, SyncConflictContext, SyncConflictReason, SyncConflictResolution, SyncCollectionOptions, SyncDeadLetter, SyncEvents, SyncIdMapping, SyncInitialMode, SyncOperation, SyncOperationType, SyncOptions, SyncOutboxRecord, SyncOverflowStrategy, SyncSink, SyncStatus, } from './types.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { SyncReplicator } from './replicator.js';
|
|
2
|
+
export { SyncOutbox, OUTBOX_TABLE, STATE_TABLE, DEAD_LETTER_TABLE, SYNC_OBJECT_PREFIX, } from './outbox.js';
|
|
3
|
+
export { MongoUpstreamSink, VERSION_FIELD, UPDATED_AT_FIELD } from './mongoSink.js';
|
|
4
|
+
export { HttpUpstreamSink, SyncRequestRejected } from './httpSink.js';
|
|
5
|
+
export { buildWriteCommand, toUpstreamId } from './commands.js';
|
|
6
|
+
export { executeWrites, fetchDocuments } from './mongoExecutor.js';
|
|
7
|
+
export { SYNC_PROTOCOL_VERSION, SyncProtocolError, encodeBody, decodeBody, toWireOperation, fromWireOperation, } from './protocol.js';
|
|
8
|
+
export { SyncShadow, SHADOW_TABLE, projectToLocalShape } from './shadow.js';
|
|
9
|
+
export { computeDiff, applyDiff, isEmptyDiff, deepEqual } from './diff.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EACL,UAAU,EACV,YAAY,EACZ,WAAW,EACX,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEpF,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEtE,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAQvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE5E,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MongoWriteCommand, ObjectIdFactory } from './commands.js';
|
|
2
|
+
import type { MongoDbLike, SyncApplyResult, SyncIdMapping, SyncOperation } from './types.js';
|
|
3
|
+
/** One operation paired with the command that realises it. */
|
|
4
|
+
export interface PreparedWrite {
|
|
5
|
+
/** Index into the caller's original operations array, echoed back in results. */
|
|
6
|
+
index: number;
|
|
7
|
+
op: SyncOperation;
|
|
8
|
+
command: MongoWriteCommand;
|
|
9
|
+
}
|
|
10
|
+
export interface ExecuteOptions {
|
|
11
|
+
writeConcern?: Record<string, unknown>;
|
|
12
|
+
versioning?: boolean;
|
|
13
|
+
idMapping?: SyncIdMapping;
|
|
14
|
+
ObjectId?: ObjectIdFactory;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Runs prepared writes against a database, one `bulkWrite` per collection.
|
|
18
|
+
*
|
|
19
|
+
* Throws on transient failure (network, election, timeout) so the caller retries the
|
|
20
|
+
* whole batch without checkpointing. Returns per-operation failures and conflicts for
|
|
21
|
+
* everything it could classify.
|
|
22
|
+
*/
|
|
23
|
+
export declare function executeWrites(db: MongoDbLike, writes: PreparedWrite[], options?: ExecuteOptions): Promise<SyncApplyResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Splits a driver error into per-operation permanent failures.
|
|
26
|
+
*
|
|
27
|
+
* @returns The failures to dead-letter, or `null` when the error is transient and the
|
|
28
|
+
* whole batch should be retried.
|
|
29
|
+
*/
|
|
30
|
+
export declare function classifyBulkError(err: unknown, operationCount: number): Array<{
|
|
31
|
+
index: number;
|
|
32
|
+
message: string;
|
|
33
|
+
code?: number | string;
|
|
34
|
+
keyPattern?: Record<string, unknown>;
|
|
35
|
+
}> | null;
|
|
36
|
+
/** Reads documents back by `_id`, for refreshing a shadow after a conflict. */
|
|
37
|
+
export declare function fetchDocuments(db: MongoDbLike, collection: string, documentIds: string[], idMapping?: SyncIdMapping, ObjectId?: ObjectIdFactory): Promise<Record<string, unknown>[]>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies prepared write commands to MongoDB and classifies the outcome.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the direct sink and by the HTTP receiver, so the subtle part has exactly
|
|
5
|
+
* one implementation: **a lost compare-and-swap raises no error.** It is a silent
|
|
6
|
+
* zero-match in an aggregate count, which means detecting it takes deliberate work —
|
|
7
|
+
* comparing what the batch expected to change against what the server says changed, and
|
|
8
|
+
* then re-reading the candidates to find out which document actually lost.
|
|
9
|
+
*/
|
|
10
|
+
import { isConditional, isReservedPath, readVersion, stripReservedFields, toUpstreamId, } from './commands.js';
|
|
11
|
+
import { deepEqual } from './diff.js';
|
|
12
|
+
/**
|
|
13
|
+
* MongoDB server error codes that mean "this write will never succeed" — retrying them
|
|
14
|
+
* would stall the stream behind one bad document forever, so they are dead-lettered.
|
|
15
|
+
*/
|
|
16
|
+
const PERMANENT_ERROR_CODES = new Set([
|
|
17
|
+
2, // BadValue
|
|
18
|
+
9, // FailedToParse
|
|
19
|
+
10334, // BSONObjectTooLarge
|
|
20
|
+
14, // TypeMismatch
|
|
21
|
+
52, // DollarPrefixedFieldName
|
|
22
|
+
56, // EmptyFieldName
|
|
23
|
+
121, // DocumentValidationFailure
|
|
24
|
+
17280, // KeyTooLong
|
|
25
|
+
]);
|
|
26
|
+
/** MongoDB's duplicate-key code. On an insert-if-absent it means we lost a create race. */
|
|
27
|
+
const DUPLICATE_KEY = 11000;
|
|
28
|
+
/**
|
|
29
|
+
* Runs prepared writes against a database, one `bulkWrite` per collection.
|
|
30
|
+
*
|
|
31
|
+
* Throws on transient failure (network, election, timeout) so the caller retries the
|
|
32
|
+
* whole batch without checkpointing. Returns per-operation failures and conflicts for
|
|
33
|
+
* everything it could classify.
|
|
34
|
+
*/
|
|
35
|
+
export async function executeWrites(db, writes, options = {}) {
|
|
36
|
+
const { writeConcern = { w: 'majority' }, versioning = true, idMapping, ObjectId } = options;
|
|
37
|
+
const byCollection = new Map();
|
|
38
|
+
for (const write of writes) {
|
|
39
|
+
const bucket = byCollection.get(write.op.collection);
|
|
40
|
+
if (bucket)
|
|
41
|
+
bucket.push(write);
|
|
42
|
+
else
|
|
43
|
+
byCollection.set(write.op.collection, [write]);
|
|
44
|
+
}
|
|
45
|
+
const failures = [];
|
|
46
|
+
const conflicts = [];
|
|
47
|
+
let applied = 0;
|
|
48
|
+
for (const [collectionName, entries] of byCollection) {
|
|
49
|
+
let result = null;
|
|
50
|
+
let rejected = 0;
|
|
51
|
+
try {
|
|
52
|
+
result = await db.collection(collectionName).bulkWrite(entries.map((entry) => entry.command), {
|
|
53
|
+
// Operations within a batch touch distinct documents, so ordering buys
|
|
54
|
+
// nothing and unordered lets the server parallelise.
|
|
55
|
+
ordered: false,
|
|
56
|
+
writeConcern,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const bulkFailures = classifyBulkError(err, entries.length);
|
|
61
|
+
// A transient failure means the whole batch's fate is unknown — throw so the
|
|
62
|
+
// caller retries it rather than checkpointing work that may not have landed.
|
|
63
|
+
if (bulkFailures === null)
|
|
64
|
+
throw err;
|
|
65
|
+
for (const failure of bulkFailures) {
|
|
66
|
+
const entry = entries[failure.index];
|
|
67
|
+
if (!entry)
|
|
68
|
+
continue;
|
|
69
|
+
// A duplicate `_id` on an insert-if-absent means someone created the document
|
|
70
|
+
// first — a conflict to reconcile, not a poison document. A duplicate on any
|
|
71
|
+
// *other* unique index is a real constraint violation and must be dead-lettered,
|
|
72
|
+
// or it would be retried forever as a phantom conflict.
|
|
73
|
+
if (failure.code === DUPLICATE_KEY && isIdIndexViolation(failure)) {
|
|
74
|
+
conflicts.push({ index: entry.index, reason: 'already-exists' });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
failures.push({ index: entry.index, message: failure.message, code: failure.code });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
rejected = bulkFailures.length;
|
|
81
|
+
result = err.result ?? null;
|
|
82
|
+
}
|
|
83
|
+
const landed = entries.length - rejected;
|
|
84
|
+
const suspects = findMisses(entries, result, rejected, versioning);
|
|
85
|
+
// `suspects` are candidates to investigate, not confirmed misses: the counts are
|
|
86
|
+
// aggregate, so one short count implicates every conditional write in the batch.
|
|
87
|
+
// Only the re-read says which of them actually lost, and `applied` must be reduced
|
|
88
|
+
// by that number rather than by the number investigated.
|
|
89
|
+
const confirmed = suspects.length
|
|
90
|
+
? await describeConflicts(db, collectionName, suspects, conflicts, idMapping, ObjectId)
|
|
91
|
+
: 0;
|
|
92
|
+
applied += landed - confirmed;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
applied,
|
|
96
|
+
failures: failures.length > 0 ? failures : undefined,
|
|
97
|
+
conflicts: conflicts.length > 0 ? conflicts : undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Identifies operations whose conditional write may not have matched.
|
|
102
|
+
*
|
|
103
|
+
* `bulkWrite` reports only aggregate counts, so this compares what the batch expected
|
|
104
|
+
* to change against what the server says changed. When the numbers agree, nothing was
|
|
105
|
+
* missed and no per-document work is needed.
|
|
106
|
+
*/
|
|
107
|
+
function findMisses(entries, result, rejected, versioning) {
|
|
108
|
+
if (!versioning || result === null || rejected === entries.length)
|
|
109
|
+
return [];
|
|
110
|
+
const candidates = entries.filter((entry) => isConditional(entry.op));
|
|
111
|
+
if (candidates.length === 0)
|
|
112
|
+
return [];
|
|
113
|
+
const writes = candidates.filter((entry) => entry.op.type !== 'delete');
|
|
114
|
+
const deletes = candidates.filter((entry) => entry.op.type === 'delete');
|
|
115
|
+
const acknowledgedWrites = (result.matchedCount ?? 0) + (result.upsertedCount ?? 0);
|
|
116
|
+
const acknowledgedDeletes = result.deletedCount ?? 0;
|
|
117
|
+
const missed = [];
|
|
118
|
+
// The counts are aggregate, so which specific documents missed is unknown; the
|
|
119
|
+
// caller re-reads the candidates and decides per document.
|
|
120
|
+
if (acknowledgedWrites < writes.length)
|
|
121
|
+
missed.push(...writes);
|
|
122
|
+
if (acknowledgedDeletes < deletes.length)
|
|
123
|
+
missed.push(...deletes);
|
|
124
|
+
return missed;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Re-reads the upstream state of operations that may have missed, and records a
|
|
128
|
+
* conflict for each one whose version really has moved on.
|
|
129
|
+
*
|
|
130
|
+
* The re-read turns an aggregate "something didn't match" into a precise, per-document
|
|
131
|
+
* answer, and supplies the current server document the caller needs to reconcile.
|
|
132
|
+
*
|
|
133
|
+
* @returns How many candidates genuinely lost.
|
|
134
|
+
*/
|
|
135
|
+
async function describeConflicts(db, collectionName, candidates, conflicts, idMapping, ObjectId) {
|
|
136
|
+
const ids = candidates.map((entry) => toUpstreamId(entry.op.documentId, idMapping, ObjectId));
|
|
137
|
+
const current = await db
|
|
138
|
+
.collection(collectionName)
|
|
139
|
+
.find({ _id: { $in: ids } })
|
|
140
|
+
.toArray();
|
|
141
|
+
const byId = new Map(current.map((doc) => [String(doc._id), doc]));
|
|
142
|
+
let confirmed = 0;
|
|
143
|
+
for (const { index, op } of candidates) {
|
|
144
|
+
const serverDocument = byId.get(op.documentId);
|
|
145
|
+
const serverVersion = readVersion(serverDocument);
|
|
146
|
+
if (op.type === 'delete') {
|
|
147
|
+
// Already gone upstream is the outcome we wanted, however it happened.
|
|
148
|
+
if (!serverDocument)
|
|
149
|
+
continue;
|
|
150
|
+
conflicts.push({ index, reason: 'version-mismatch', serverDocument, serverVersion });
|
|
151
|
+
confirmed += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!serverDocument) {
|
|
155
|
+
conflicts.push({ index, reason: 'missing' });
|
|
156
|
+
confirmed += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// The write may have landed after all, with a different operation in the batch
|
|
160
|
+
// being the one that missed.
|
|
161
|
+
//
|
|
162
|
+
// A version one ahead is necessary evidence but nowhere near sufficient: another
|
|
163
|
+
// writer following the same protocol also bumps `_v` by exactly one, so the two
|
|
164
|
+
// cases look identical from the version alone. Confirming that the document
|
|
165
|
+
// actually carries our change is what tells them apart — and getting this wrong
|
|
166
|
+
// silently swallows the very conflicts this machinery exists to catch.
|
|
167
|
+
if (isConditional(op) &&
|
|
168
|
+
serverVersion === op.baseVersion + 1 &&
|
|
169
|
+
reflectsWrite(serverDocument, op)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
conflicts.push({ index, reason: 'version-mismatch', serverDocument, serverVersion });
|
|
173
|
+
confirmed += 1;
|
|
174
|
+
}
|
|
175
|
+
return confirmed;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Splits a driver error into per-operation permanent failures.
|
|
179
|
+
*
|
|
180
|
+
* @returns The failures to dead-letter, or `null` when the error is transient and the
|
|
181
|
+
* whole batch should be retried.
|
|
182
|
+
*/
|
|
183
|
+
export function classifyBulkError(err, operationCount) {
|
|
184
|
+
const error = err;
|
|
185
|
+
// The driver retried and still could not reach a writable primary.
|
|
186
|
+
if (error.hasErrorLabel?.('RetryableWriteError'))
|
|
187
|
+
return null;
|
|
188
|
+
const writeErrors = error.writeErrors ?? error.result?.writeErrors;
|
|
189
|
+
if (Array.isArray(writeErrors) && writeErrors.length > 0) {
|
|
190
|
+
const failures = [];
|
|
191
|
+
for (const writeError of writeErrors) {
|
|
192
|
+
const code = writeError.code;
|
|
193
|
+
// An unrecognised write error might be transient (a duplicate key during a
|
|
194
|
+
// failover, say), so the batch is worth retrying whole. A duplicate key is
|
|
195
|
+
// handled separately by the caller, as a create race rather than a defect.
|
|
196
|
+
if (code === undefined || (code !== DUPLICATE_KEY && !PERMANENT_ERROR_CODES.has(code))) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const index = writeError.index ?? 0;
|
|
200
|
+
if (index >= operationCount)
|
|
201
|
+
return null;
|
|
202
|
+
failures.push({
|
|
203
|
+
index,
|
|
204
|
+
message: writeError.errmsg ?? writeError.message ?? `write error ${code}`,
|
|
205
|
+
code,
|
|
206
|
+
keyPattern: writeError.keyPattern,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return failures;
|
|
210
|
+
}
|
|
211
|
+
// No per-write detail: a connection, timeout or auth error. Retry it.
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* True when the upstream document carries the change this operation was making.
|
|
216
|
+
*
|
|
217
|
+
* Only the paths the operation itself wrote are checked. That is deliberate: those are
|
|
218
|
+
* the values we would have written in local JSON form, so comparing them against the
|
|
219
|
+
* server is meaningful, whereas fields we never touched may hold BSON types the local
|
|
220
|
+
* store cannot represent and would not compare equal.
|
|
221
|
+
*
|
|
222
|
+
* A false positive is possible if another writer happened to set the identical value —
|
|
223
|
+
* and is harmless, since the upstream then holds what we wanted anyway.
|
|
224
|
+
*/
|
|
225
|
+
function reflectsWrite(serverDocument, op) {
|
|
226
|
+
if (op.diff) {
|
|
227
|
+
for (const [path, value] of Object.entries(op.diff.set)) {
|
|
228
|
+
if (isReservedPath(path))
|
|
229
|
+
continue;
|
|
230
|
+
if (!valueAtPathEquals(serverDocument, path, value))
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
for (const path of op.diff.unset) {
|
|
234
|
+
if (isReservedPath(path))
|
|
235
|
+
continue;
|
|
236
|
+
if (readPath(serverDocument, path) !== undefined)
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
// Whole-document write: every field we sent should be present upstream.
|
|
242
|
+
for (const [key, value] of Object.entries(stripReservedFields(op.document ?? {}))) {
|
|
243
|
+
if (!valueAtPathEquals(serverDocument, key, value))
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Compares a value at a dotted path against what we wrote.
|
|
250
|
+
*
|
|
251
|
+
* The server side is flattened to local JSON shape first, so a `Date` upstream compares
|
|
252
|
+
* equal to the ISO string a local write would have carried.
|
|
253
|
+
*/
|
|
254
|
+
function valueAtPathEquals(document, path, expected) {
|
|
255
|
+
const actual = readPath(document, path);
|
|
256
|
+
if (actual === undefined)
|
|
257
|
+
return false;
|
|
258
|
+
try {
|
|
259
|
+
return deepEqual(JSON.parse(JSON.stringify(actual)), expected);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** Reads a dotted path, returning `undefined` if any segment is missing. */
|
|
266
|
+
function readPath(document, path) {
|
|
267
|
+
let current = document;
|
|
268
|
+
for (const segment of path.split('.')) {
|
|
269
|
+
if (typeof current !== 'object' || current === null)
|
|
270
|
+
return undefined;
|
|
271
|
+
current = current[segment];
|
|
272
|
+
if (current === undefined)
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return current;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* True when a duplicate-key error is about the `_id` index specifically.
|
|
279
|
+
*
|
|
280
|
+
* MongoDB reports the offending index in `keyPattern` (and names it in `errmsg`), which
|
|
281
|
+
* is the only way to tell "someone else created this document" apart from "this document
|
|
282
|
+
* violates a unique constraint on some other field".
|
|
283
|
+
*/
|
|
284
|
+
function isIdIndexViolation(failure) {
|
|
285
|
+
if (failure.keyPattern) {
|
|
286
|
+
const keys = Object.keys(failure.keyPattern);
|
|
287
|
+
return keys.length === 1 && keys[0] === '_id';
|
|
288
|
+
}
|
|
289
|
+
// Older servers and some drivers only give the message; the index name appears in it.
|
|
290
|
+
return /index:\s*_id_/.test(failure.message);
|
|
291
|
+
}
|
|
292
|
+
/** Reads documents back by `_id`, for refreshing a shadow after a conflict. */
|
|
293
|
+
export async function fetchDocuments(db, collection, documentIds, idMapping, ObjectId) {
|
|
294
|
+
if (documentIds.length === 0)
|
|
295
|
+
return [];
|
|
296
|
+
const ids = documentIds.map((id) => toUpstreamId(id, idMapping, ObjectId));
|
|
297
|
+
return db
|
|
298
|
+
.collection(collection)
|
|
299
|
+
.find({ _id: { $in: ids } })
|
|
300
|
+
.toArray();
|
|
301
|
+
}
|
|
302
|
+
//# sourceMappingURL=mongoExecutor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongoExecutor.js","sourceRoot":"","sources":["../../src/sync/mongoExecutor.ts"],"names":[],"mappings":"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"}
|