@semics-tech/mongolite 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -21
- package/dist/cjs/collection.js +8 -1
- 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 +8 -1
- package/dist/collection.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 +15 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SyncProtocolError = exports.SYNC_CONTENT_TYPE = exports.SYNC_PROTOCOL_VERSION = void 0;
|
|
4
|
+
exports.encodeBody = encodeBody;
|
|
5
|
+
exports.decodeBody = decodeBody;
|
|
6
|
+
exports.toWireOperation = toWireOperation;
|
|
7
|
+
exports.fromWireOperation = fromWireOperation;
|
|
8
|
+
exports.parseApplyRequest = parseApplyRequest;
|
|
9
|
+
exports.parseFetchRequest = parseFetchRequest;
|
|
10
|
+
/**
|
|
11
|
+
* The wire format shared by the HTTP sink and the receiver that applies its messages.
|
|
12
|
+
*
|
|
13
|
+
* Both halves live in this package on purpose: a client and a receiver that disagree
|
|
14
|
+
* about the protocol lose data quietly, so they are versioned and released together.
|
|
15
|
+
*
|
|
16
|
+
* Bodies are **relaxed Extended JSON**, not plain JSON. Plain JSON would flatten
|
|
17
|
+
* `ObjectId` to a string and `Date` to an ISO string in transit — degrading exactly the
|
|
18
|
+
* BSON types the sync design goes to some trouble to preserve, and doing it invisibly.
|
|
19
|
+
*
|
|
20
|
+
* Relaxed rather than canonical, because canonical encodes every number as `$numberInt`
|
|
21
|
+
* or `$numberDouble` and revives it as a BSON wrapper object. A local `age: 31` would
|
|
22
|
+
* then be written upstream as an `Int32` over HTTP but as a double over a direct
|
|
23
|
+
* connection — the same change producing different stored types depending on transport.
|
|
24
|
+
* Relaxed keeps plain numbers plain while still preserving `Date`, `ObjectId`, `Binary`
|
|
25
|
+
* and `Decimal128`. The cost is that 64-bit integers beyond 2^53 lose precision, which
|
|
26
|
+
* the local store could not represent anyway.
|
|
27
|
+
*/
|
|
28
|
+
const bson_1 = require("bson");
|
|
29
|
+
/**
|
|
30
|
+
* Protocol version, sent with every request.
|
|
31
|
+
*
|
|
32
|
+
* A receiver rejects a version it does not understand rather than guessing, so an
|
|
33
|
+
* upgraded client talking to an old server fails loudly instead of silently writing
|
|
34
|
+
* something unintended.
|
|
35
|
+
*/
|
|
36
|
+
exports.SYNC_PROTOCOL_VERSION = 1;
|
|
37
|
+
/** Content type used for request and response bodies. */
|
|
38
|
+
exports.SYNC_CONTENT_TYPE = 'application/json';
|
|
39
|
+
/** Raised when a payload is malformed or speaks a version we do not implement. */
|
|
40
|
+
class SyncProtocolError extends Error {
|
|
41
|
+
constructor(message,
|
|
42
|
+
/** Suggested HTTP status for a receiver to return. */
|
|
43
|
+
status = 400) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.status = status;
|
|
46
|
+
this.name = 'SyncProtocolError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.SyncProtocolError = SyncProtocolError;
|
|
50
|
+
/** Serialises a body to relaxed Extended JSON. */
|
|
51
|
+
function encodeBody(value) {
|
|
52
|
+
return bson_1.EJSON.stringify(value, { relaxed: true });
|
|
53
|
+
}
|
|
54
|
+
/** Parses a relaxed Extended JSON body back into BSON-typed values. */
|
|
55
|
+
function decodeBody(raw) {
|
|
56
|
+
if (typeof raw !== 'string') {
|
|
57
|
+
// Some frameworks parse JSON before we see it. Re-encoding is the only way to get
|
|
58
|
+
// Extended JSON revived into real BSON types.
|
|
59
|
+
return bson_1.EJSON.parse(JSON.stringify(raw), { relaxed: true });
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return bson_1.EJSON.parse(raw, { relaxed: true });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new SyncProtocolError(`Malformed sync payload: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Converts an internal operation into its wire form. */
|
|
69
|
+
function toWireOperation(op, command) {
|
|
70
|
+
const wire = {
|
|
71
|
+
collection: op.collection,
|
|
72
|
+
documentId: op.documentId,
|
|
73
|
+
type: op.type,
|
|
74
|
+
};
|
|
75
|
+
if (op.baseVersion !== undefined)
|
|
76
|
+
wire.baseVersion = op.baseVersion;
|
|
77
|
+
if (op.diff)
|
|
78
|
+
wire.diff = op.diff;
|
|
79
|
+
if (op.document)
|
|
80
|
+
wire.document = op.document;
|
|
81
|
+
if (command)
|
|
82
|
+
wire.command = command;
|
|
83
|
+
return wire;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validates a wire operation and converts it back into an internal one.
|
|
87
|
+
*
|
|
88
|
+
* Everything reaching a receiver is untrusted input, so this checks shape rather than
|
|
89
|
+
* assuming it. `index` and `sourceCollection` are reconstructed locally — a client does
|
|
90
|
+
* not get to choose them.
|
|
91
|
+
*/
|
|
92
|
+
function fromWireOperation(value, index) {
|
|
93
|
+
if (typeof value !== 'object' || value === null) {
|
|
94
|
+
throw new SyncProtocolError(`operations[${index}] is not an object`);
|
|
95
|
+
}
|
|
96
|
+
const wire = value;
|
|
97
|
+
if (typeof wire.collection !== 'string' || wire.collection.length === 0) {
|
|
98
|
+
throw new SyncProtocolError(`operations[${index}].collection must be a non-empty string`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof wire.documentId !== 'string' || wire.documentId.length === 0) {
|
|
101
|
+
throw new SyncProtocolError(`operations[${index}].documentId must be a non-empty string`);
|
|
102
|
+
}
|
|
103
|
+
if (wire.type !== 'upsert' && wire.type !== 'delete') {
|
|
104
|
+
throw new SyncProtocolError(`operations[${index}].type must be "upsert" or "delete"`);
|
|
105
|
+
}
|
|
106
|
+
if (wire.baseVersion !== undefined &&
|
|
107
|
+
wire.baseVersion !== null &&
|
|
108
|
+
typeof wire.baseVersion !== 'number') {
|
|
109
|
+
throw new SyncProtocolError(`operations[${index}].baseVersion must be a number or null`);
|
|
110
|
+
}
|
|
111
|
+
const op = {
|
|
112
|
+
collection: wire.collection,
|
|
113
|
+
sourceCollection: wire.collection,
|
|
114
|
+
documentId: wire.documentId,
|
|
115
|
+
type: wire.type,
|
|
116
|
+
outboxId: index,
|
|
117
|
+
baseVersion: wire.baseVersion ?? null,
|
|
118
|
+
};
|
|
119
|
+
if (isPlainRecord(wire.document))
|
|
120
|
+
op.document = wire.document;
|
|
121
|
+
if (wire.diff !== undefined) {
|
|
122
|
+
if (!isPlainRecord(wire.diff)) {
|
|
123
|
+
throw new SyncProtocolError(`operations[${index}].diff must be an object`);
|
|
124
|
+
}
|
|
125
|
+
const set = wire.diff.set;
|
|
126
|
+
const unset = wire.diff.unset;
|
|
127
|
+
if (!isPlainRecord(set) || !Array.isArray(unset) || unset.some((p) => typeof p !== 'string')) {
|
|
128
|
+
throw new SyncProtocolError(`operations[${index}].diff must be { set: object, unset: string[] }`);
|
|
129
|
+
}
|
|
130
|
+
op.diff = { set, unset: unset };
|
|
131
|
+
}
|
|
132
|
+
if (op.type === 'upsert' && !op.diff && !op.document) {
|
|
133
|
+
throw new SyncProtocolError(`operations[${index}] must carry a diff or a document`);
|
|
134
|
+
}
|
|
135
|
+
return op;
|
|
136
|
+
}
|
|
137
|
+
/** Validates the envelope of an apply request and returns its operations. */
|
|
138
|
+
function parseApplyRequest(body) {
|
|
139
|
+
const request = requireEnvelope(body);
|
|
140
|
+
const operations = request.operations;
|
|
141
|
+
if (!Array.isArray(operations)) {
|
|
142
|
+
throw new SyncProtocolError('`operations` must be an array');
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
replicator: typeof request.replicator === 'string' ? request.replicator : 'unknown',
|
|
146
|
+
operations: operations.map((value, index) => fromWireOperation(value, index)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Validates the envelope of a fetch request. */
|
|
150
|
+
function parseFetchRequest(body) {
|
|
151
|
+
const request = requireEnvelope(body);
|
|
152
|
+
if (typeof request.collection !== 'string' || request.collection.length === 0) {
|
|
153
|
+
throw new SyncProtocolError('`collection` must be a non-empty string');
|
|
154
|
+
}
|
|
155
|
+
if (!Array.isArray(request.documentIds) ||
|
|
156
|
+
request.documentIds.some((id) => typeof id !== 'string')) {
|
|
157
|
+
throw new SyncProtocolError('`documentIds` must be an array of strings');
|
|
158
|
+
}
|
|
159
|
+
return { collection: request.collection, documentIds: request.documentIds };
|
|
160
|
+
}
|
|
161
|
+
function requireEnvelope(body) {
|
|
162
|
+
if (!isPlainRecord(body)) {
|
|
163
|
+
throw new SyncProtocolError('Request body must be an object');
|
|
164
|
+
}
|
|
165
|
+
// Coerced rather than compared strictly: a proxy that re-encodes the body could hand
|
|
166
|
+
// us the version as a string or a BSON number wrapper, and refusing on that would be
|
|
167
|
+
// a confusing failure for something entirely cosmetic.
|
|
168
|
+
if (Number(body.protocol) !== exports.SYNC_PROTOCOL_VERSION) {
|
|
169
|
+
throw new SyncProtocolError(`Unsupported sync protocol ${String(body.protocol)}; this receiver implements ${exports.SYNC_PROTOCOL_VERSION}`,
|
|
170
|
+
// 426 tells the client the fix is to upgrade, not to retry.
|
|
171
|
+
426);
|
|
172
|
+
}
|
|
173
|
+
return body;
|
|
174
|
+
}
|
|
175
|
+
function isPlainRecord(value) {
|
|
176
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
177
|
+
}
|