@semics-tech/mongolite 1.2.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.
Files changed (99) hide show
  1. package/README.md +91 -20
  2. package/dist/bin/mongolite-debug.d.ts +6 -1
  3. package/dist/bin/mongolite-debug.js +6 -113
  4. package/dist/bin/mongolite-debug.js.map +1 -1
  5. package/dist/bin/mongolite.d.ts +13 -0
  6. package/dist/bin/mongolite.js +172 -0
  7. package/dist/bin/mongolite.js.map +1 -0
  8. package/dist/cjs/bin/mongolite-debug.js +6 -149
  9. package/dist/cjs/bin/mongolite.js +207 -0
  10. package/dist/cjs/cli/discover.js +156 -0
  11. package/dist/cjs/cli/explorer.js +929 -0
  12. package/dist/cjs/cli/filter-builder.js +276 -0
  13. package/dist/cjs/cli/format.js +161 -0
  14. package/dist/cjs/cli/schema.js +211 -0
  15. package/dist/cjs/cli/ui.js +431 -0
  16. package/dist/cjs/collection.js +8 -1
  17. package/dist/cjs/cursors/findCursor.js +20 -8
  18. package/dist/cjs/index.js +108 -1
  19. package/dist/cjs/mongo-client.js +18 -1
  20. package/dist/cjs/server.js +164 -0
  21. package/dist/cjs/sync/commands.js +126 -0
  22. package/dist/cjs/sync/diff.js +174 -0
  23. package/dist/cjs/sync/httpSink.js +159 -0
  24. package/dist/cjs/sync/index.js +40 -0
  25. package/dist/cjs/sync/mongoExecutor.js +306 -0
  26. package/dist/cjs/sync/mongoSink.js +161 -0
  27. package/dist/cjs/sync/outbox.js +358 -0
  28. package/dist/cjs/sync/protocol.js +177 -0
  29. package/dist/cjs/sync/replicator.js +747 -0
  30. package/dist/cjs/sync/shadow.js +178 -0
  31. package/dist/cjs/sync/types.js +2 -0
  32. package/dist/cli/discover.d.ts +40 -0
  33. package/dist/cli/discover.js +146 -0
  34. package/dist/cli/discover.js.map +1 -0
  35. package/dist/cli/explorer.d.ts +59 -0
  36. package/dist/cli/explorer.js +923 -0
  37. package/dist/cli/explorer.js.map +1 -0
  38. package/dist/cli/filter-builder.d.ts +52 -0
  39. package/dist/cli/filter-builder.js +268 -0
  40. package/dist/cli/filter-builder.js.map +1 -0
  41. package/dist/cli/format.d.ts +29 -0
  42. package/dist/cli/format.js +153 -0
  43. package/dist/cli/format.js.map +1 -0
  44. package/dist/cli/schema.d.ts +58 -0
  45. package/dist/cli/schema.js +205 -0
  46. package/dist/cli/schema.js.map +1 -0
  47. package/dist/cli/ui.d.ts +71 -0
  48. package/dist/cli/ui.js +379 -0
  49. package/dist/cli/ui.js.map +1 -0
  50. package/dist/cloudflare.d.ts +1 -1
  51. package/dist/cloudflare.js.map +1 -1
  52. package/dist/collection.js +8 -1
  53. package/dist/collection.js.map +1 -1
  54. package/dist/cursors/findCursor.d.ts +12 -0
  55. package/dist/cursors/findCursor.js +20 -8
  56. package/dist/cursors/findCursor.js.map +1 -1
  57. package/dist/index.d.ts +68 -2
  58. package/dist/index.js +96 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/mongo-client.d.ts +13 -0
  61. package/dist/mongo-client.js +18 -1
  62. package/dist/mongo-client.js.map +1 -1
  63. package/dist/server.d.ts +97 -0
  64. package/dist/server.js +158 -0
  65. package/dist/server.js.map +1 -0
  66. package/dist/sync/commands.d.ts +49 -0
  67. package/dist/sync/commands.js +118 -0
  68. package/dist/sync/commands.js.map +1 -0
  69. package/dist/sync/diff.d.ts +55 -0
  70. package/dist/sync/diff.js +169 -0
  71. package/dist/sync/diff.js.map +1 -0
  72. package/dist/sync/httpSink.d.ts +93 -0
  73. package/dist/sync/httpSink.js +155 -0
  74. package/dist/sync/httpSink.js.map +1 -0
  75. package/dist/sync/index.d.ts +17 -0
  76. package/dist/sync/index.js +10 -0
  77. package/dist/sync/index.js.map +1 -0
  78. package/dist/sync/mongoExecutor.d.ts +37 -0
  79. package/dist/sync/mongoExecutor.js +302 -0
  80. package/dist/sync/mongoExecutor.js.map +1 -0
  81. package/dist/sync/mongoSink.d.ts +109 -0
  82. package/dist/sync/mongoSink.js +123 -0
  83. package/dist/sync/mongoSink.js.map +1 -0
  84. package/dist/sync/outbox.d.ts +120 -0
  85. package/dist/sync/outbox.js +355 -0
  86. package/dist/sync/outbox.js.map +1 -0
  87. package/dist/sync/protocol.d.ts +93 -0
  88. package/dist/sync/protocol.js +168 -0
  89. package/dist/sync/protocol.js.map +1 -0
  90. package/dist/sync/replicator.d.ts +177 -0
  91. package/dist/sync/replicator.js +744 -0
  92. package/dist/sync/replicator.js.map +1 -0
  93. package/dist/sync/shadow.d.ts +54 -0
  94. package/dist/sync/shadow.js +173 -0
  95. package/dist/sync/shadow.js.map +1 -0
  96. package/dist/sync/types.d.ts +388 -0
  97. package/dist/sync/types.js +2 -0
  98. package/dist/sync/types.js.map +1 -0
  99. package/package.json +16 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The wire format shared by the HTTP sink and the receiver that applies its messages.
3
+ *
4
+ * Both halves live in this package on purpose: a client and a receiver that disagree
5
+ * about the protocol lose data quietly, so they are versioned and released together.
6
+ *
7
+ * Bodies are **relaxed Extended JSON**, not plain JSON. Plain JSON would flatten
8
+ * `ObjectId` to a string and `Date` to an ISO string in transit — degrading exactly the
9
+ * BSON types the sync design goes to some trouble to preserve, and doing it invisibly.
10
+ *
11
+ * Relaxed rather than canonical, because canonical encodes every number as `$numberInt`
12
+ * or `$numberDouble` and revives it as a BSON wrapper object. A local `age: 31` would
13
+ * then be written upstream as an `Int32` over HTTP but as a double over a direct
14
+ * connection — the same change producing different stored types depending on transport.
15
+ * Relaxed keeps plain numbers plain while still preserving `Date`, `ObjectId`, `Binary`
16
+ * and `Decimal128`. The cost is that 64-bit integers beyond 2^53 lose precision, which
17
+ * the local store could not represent anyway.
18
+ */
19
+ import { EJSON } from 'bson';
20
+ /**
21
+ * Protocol version, sent with every request.
22
+ *
23
+ * A receiver rejects a version it does not understand rather than guessing, so an
24
+ * upgraded client talking to an old server fails loudly instead of silently writing
25
+ * something unintended.
26
+ */
27
+ export const SYNC_PROTOCOL_VERSION = 1;
28
+ /** Content type used for request and response bodies. */
29
+ export const SYNC_CONTENT_TYPE = 'application/json';
30
+ /** Raised when a payload is malformed or speaks a version we do not implement. */
31
+ export class SyncProtocolError extends Error {
32
+ constructor(message,
33
+ /** Suggested HTTP status for a receiver to return. */
34
+ status = 400) {
35
+ super(message);
36
+ this.status = status;
37
+ this.name = 'SyncProtocolError';
38
+ }
39
+ }
40
+ /** Serialises a body to relaxed Extended JSON. */
41
+ export function encodeBody(value) {
42
+ return EJSON.stringify(value, { relaxed: true });
43
+ }
44
+ /** Parses a relaxed Extended JSON body back into BSON-typed values. */
45
+ export function decodeBody(raw) {
46
+ if (typeof raw !== 'string') {
47
+ // Some frameworks parse JSON before we see it. Re-encoding is the only way to get
48
+ // Extended JSON revived into real BSON types.
49
+ return EJSON.parse(JSON.stringify(raw), { relaxed: true });
50
+ }
51
+ try {
52
+ return EJSON.parse(raw, { relaxed: true });
53
+ }
54
+ catch (err) {
55
+ throw new SyncProtocolError(`Malformed sync payload: ${err instanceof Error ? err.message : String(err)}`);
56
+ }
57
+ }
58
+ /** Converts an internal operation into its wire form. */
59
+ export function toWireOperation(op, command) {
60
+ const wire = {
61
+ collection: op.collection,
62
+ documentId: op.documentId,
63
+ type: op.type,
64
+ };
65
+ if (op.baseVersion !== undefined)
66
+ wire.baseVersion = op.baseVersion;
67
+ if (op.diff)
68
+ wire.diff = op.diff;
69
+ if (op.document)
70
+ wire.document = op.document;
71
+ if (command)
72
+ wire.command = command;
73
+ return wire;
74
+ }
75
+ /**
76
+ * Validates a wire operation and converts it back into an internal one.
77
+ *
78
+ * Everything reaching a receiver is untrusted input, so this checks shape rather than
79
+ * assuming it. `index` and `sourceCollection` are reconstructed locally — a client does
80
+ * not get to choose them.
81
+ */
82
+ export function fromWireOperation(value, index) {
83
+ if (typeof value !== 'object' || value === null) {
84
+ throw new SyncProtocolError(`operations[${index}] is not an object`);
85
+ }
86
+ const wire = value;
87
+ if (typeof wire.collection !== 'string' || wire.collection.length === 0) {
88
+ throw new SyncProtocolError(`operations[${index}].collection must be a non-empty string`);
89
+ }
90
+ if (typeof wire.documentId !== 'string' || wire.documentId.length === 0) {
91
+ throw new SyncProtocolError(`operations[${index}].documentId must be a non-empty string`);
92
+ }
93
+ if (wire.type !== 'upsert' && wire.type !== 'delete') {
94
+ throw new SyncProtocolError(`operations[${index}].type must be "upsert" or "delete"`);
95
+ }
96
+ if (wire.baseVersion !== undefined &&
97
+ wire.baseVersion !== null &&
98
+ typeof wire.baseVersion !== 'number') {
99
+ throw new SyncProtocolError(`operations[${index}].baseVersion must be a number or null`);
100
+ }
101
+ const op = {
102
+ collection: wire.collection,
103
+ sourceCollection: wire.collection,
104
+ documentId: wire.documentId,
105
+ type: wire.type,
106
+ outboxId: index,
107
+ baseVersion: wire.baseVersion ?? null,
108
+ };
109
+ if (isPlainRecord(wire.document))
110
+ op.document = wire.document;
111
+ if (wire.diff !== undefined) {
112
+ if (!isPlainRecord(wire.diff)) {
113
+ throw new SyncProtocolError(`operations[${index}].diff must be an object`);
114
+ }
115
+ const set = wire.diff.set;
116
+ const unset = wire.diff.unset;
117
+ if (!isPlainRecord(set) || !Array.isArray(unset) || unset.some((p) => typeof p !== 'string')) {
118
+ throw new SyncProtocolError(`operations[${index}].diff must be { set: object, unset: string[] }`);
119
+ }
120
+ op.diff = { set, unset: unset };
121
+ }
122
+ if (op.type === 'upsert' && !op.diff && !op.document) {
123
+ throw new SyncProtocolError(`operations[${index}] must carry a diff or a document`);
124
+ }
125
+ return op;
126
+ }
127
+ /** Validates the envelope of an apply request and returns its operations. */
128
+ export function parseApplyRequest(body) {
129
+ const request = requireEnvelope(body);
130
+ const operations = request.operations;
131
+ if (!Array.isArray(operations)) {
132
+ throw new SyncProtocolError('`operations` must be an array');
133
+ }
134
+ return {
135
+ replicator: typeof request.replicator === 'string' ? request.replicator : 'unknown',
136
+ operations: operations.map((value, index) => fromWireOperation(value, index)),
137
+ };
138
+ }
139
+ /** Validates the envelope of a fetch request. */
140
+ export function parseFetchRequest(body) {
141
+ const request = requireEnvelope(body);
142
+ if (typeof request.collection !== 'string' || request.collection.length === 0) {
143
+ throw new SyncProtocolError('`collection` must be a non-empty string');
144
+ }
145
+ if (!Array.isArray(request.documentIds) ||
146
+ request.documentIds.some((id) => typeof id !== 'string')) {
147
+ throw new SyncProtocolError('`documentIds` must be an array of strings');
148
+ }
149
+ return { collection: request.collection, documentIds: request.documentIds };
150
+ }
151
+ function requireEnvelope(body) {
152
+ if (!isPlainRecord(body)) {
153
+ throw new SyncProtocolError('Request body must be an object');
154
+ }
155
+ // Coerced rather than compared strictly: a proxy that re-encodes the body could hand
156
+ // us the version as a string or a BSON number wrapper, and refusing on that would be
157
+ // a confusing failure for something entirely cosmetic.
158
+ if (Number(body.protocol) !== SYNC_PROTOCOL_VERSION) {
159
+ throw new SyncProtocolError(`Unsupported sync protocol ${String(body.protocol)}; this receiver implements ${SYNC_PROTOCOL_VERSION}`,
160
+ // 426 tells the client the fix is to upgrade, not to retry.
161
+ 426);
162
+ }
163
+ return body;
164
+ }
165
+ function isPlainRecord(value) {
166
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
167
+ }
168
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/sync/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAI7B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,yDAAyD;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAqDpD,kFAAkF;AAClF,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YACE,OAAe;IACf,sDAAsD;IAC7C,SAAS,GAAG;QAErB,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,WAAM,GAAN,MAAM,CAAM;QAGrB,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,kDAAkD;AAClD,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,UAAU,CAAI,GAAqC;IACjE,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,kFAAkF;QAClF,8CAA8C;QAC9C,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAM,CAAC;IAClE,CAAC;IACD,IAAI,CAAC;QACH,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAM,CAAC;IAClD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,iBAAiB,CACzB,2BAA2B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC9E,CAAC;IACJ,CAAC;AACH,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,eAAe,CAAC,EAAiB,EAAE,OAA2B;IAC5E,MAAM,IAAI,GAAkB;QAC1B,UAAU,EAAE,EAAE,CAAC,UAAU;QACzB,UAAU,EAAE,EAAE,CAAC,UAAU;QACzB,IAAI,EAAE,EAAE,CAAC,IAAI;KACd,CAAC;IAEF,IAAI,EAAE,CAAC,WAAW,KAAK,SAAS;QAAE,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC;IACpE,IAAI,EAAE,CAAC,IAAI;QAAE,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC;IACjC,IAAI,EAAE,CAAC,QAAQ;QAAE,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC,QAAQ,CAAC;IAC7C,IAAI,OAAO;QAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAEpC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,KAAa;IAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,IAAI,iBAAiB,CAAC,cAAc,KAAK,oBAAoB,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,IAAI,GAAG,KAAgC,CAAC;IAE9C,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,iBAAiB,CAAC,cAAc,KAAK,yCAAyC,CAAC,CAAC;IAC5F,CAAC;IACD,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,iBAAiB,CAAC,cAAc,KAAK,yCAAyC,CAAC,CAAC;IAC5F,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrD,MAAM,IAAI,iBAAiB,CAAC,cAAc,KAAK,qCAAqC,CAAC,CAAC;IACxF,CAAC;IACD,IACE,IAAI,CAAC,WAAW,KAAK,SAAS;QAC9B,IAAI,CAAC,WAAW,KAAK,IAAI;QACzB,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,EACpC,CAAC;QACD,MAAM,IAAI,iBAAiB,CAAC,cAAc,KAAK,wCAAwC,CAAC,CAAC;IAC3F,CAAC;IAED,MAAM,EAAE,GAAkB;QACxB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,gBAAgB,EAAE,IAAI,CAAC,UAAU;QACjC,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,KAAK;QACf,WAAW,EAAG,IAAI,CAAC,WAAyC,IAAI,IAAI;KACrE,CAAC;IAEF,IAAI,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,EAAE,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAE9D,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,iBAAiB,CAAC,cAAc,KAAK,0BAA0B,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC;YAC7F,MAAM,IAAI,iBAAiB,CACzB,cAAc,KAAK,iDAAiD,CACrE,CAAC;QACJ,CAAC;QACD,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,KAAiB,EAAE,CAAC;IAC9C,CAAC;IAED,IAAI,EAAE,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC;QACrD,MAAM,IAAI,iBAAiB,CAAC,cAAc,KAAK,mCAAmC,CAAC,CAAC;IACtF,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAI7C,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAEtC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,iBAAiB,CAAC,+BAA+B,CAAC,CAAC;IAC/D,CAAC;IAED,OAAO;QACL,UAAU,EAAE,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QACnF,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;KAC9E,CAAC;AACJ,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAEtC,IAAI,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9E,MAAM,IAAI,iBAAiB,CAAC,yCAAyC,CAAC,CAAC;IACzE,CAAC;IACD,IACE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC;QACnC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,EACxD,CAAC;QACD,MAAM,IAAI,iBAAiB,CAAC,2CAA2C,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC,WAAuB,EAAE,CAAC;AAC1F,CAAC;AAED,SAAS,eAAe,CAAC,IAAa;IACpC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,iBAAiB,CAAC,gCAAgC,CAAC,CAAC;IAChE,CAAC;IACD,qFAAqF;IACrF,qFAAqF;IACrF,uDAAuD;IACvD,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,qBAAqB,EAAE,CAAC;QACpD,MAAM,IAAI,iBAAiB,CACzB,6BAA6B,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,8BAA8B,qBAAqB,EAAE;QACvG,4DAA4D;QAC5D,GAAG,CACJ,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Drains the {@link SyncOutbox} into an upstream {@link SyncSink}.
3
+ *
4
+ * ## Delivery guarantees
5
+ *
6
+ * Delivery is **at-least-once**, and every operation the replicator emits is
7
+ * idempotent (`replaceOne` with `upsert`, or `deleteOne` by `_id`). Replaying a
8
+ * batch therefore converges on the same upstream state, which is what makes it
9
+ * safe to acknowledge *after* the upstream write rather than before: a crash in
10
+ * the window between the two costs a duplicate write, never a lost one.
11
+ *
12
+ * ## Ordering
13
+ *
14
+ * Changes are read in outbox order and coalesced per document, so the newest
15
+ * revision wins. Within a batch, operations touch distinct documents and are
16
+ * applied unordered for throughput; across batches, order is preserved.
17
+ */
18
+ import { EventEmitter } from 'events';
19
+ import type { IDatabaseAdapter } from '../db.js';
20
+ import { SyncOutbox } from './outbox.js';
21
+ import { SyncShadow } from './shadow.js';
22
+ import type { SyncDeadLetter, SyncOptions, SyncSink, SyncStatus } from './types.js';
23
+ export declare class SyncReplicator extends EventEmitter {
24
+ private readonly db;
25
+ private readonly sink;
26
+ private readonly outbox;
27
+ private readonly shadow;
28
+ private readonly opts;
29
+ private running;
30
+ private connected;
31
+ private loop;
32
+ private checkpoint;
33
+ private backfilled;
34
+ private watched;
35
+ private lastCollectionScan;
36
+ private appliedCount;
37
+ private retryCount;
38
+ private deadLetterCount;
39
+ private conflictCount;
40
+ private lastError;
41
+ /** Resolves the current sleep early when a local write arrives or `stop()` is called. */
42
+ private wake;
43
+ constructor(db: IDatabaseAdapter, sink: SyncSink, options?: SyncOptions);
44
+ /** The outbox backing this replicator — for dead-letter inspection and manual maintenance. */
45
+ get log(): SyncOutbox;
46
+ /** The shadow store holding last-known upstream state — for diagnostics and reseeding. */
47
+ get baseline(): SyncShadow;
48
+ /**
49
+ * Installs capture triggers, backfills existing documents if this replicator
50
+ * has never run, and starts streaming changes upstream.
51
+ *
52
+ * Resolves once replication is *running*, not once it has caught up — use
53
+ * {@link SyncReplicator.waitForDrain} for that.
54
+ */
55
+ start(): Promise<void>;
56
+ /**
57
+ * Stops replication. Capture triggers stay installed, so changes made while
58
+ * stopped are still recorded and picked up on the next `start()`.
59
+ *
60
+ * @param options.flush Drain everything pending before stopping. Defaults to `false`.
61
+ */
62
+ stop(options?: {
63
+ flush?: boolean;
64
+ }): Promise<void>;
65
+ /**
66
+ * Nudges the replicator to check the outbox now instead of waiting for the
67
+ * next poll. Called by the client after local writes so replication latency
68
+ * is bounded by the upstream round trip, not the poll interval.
69
+ */
70
+ notify(): void;
71
+ /**
72
+ * Resolves once every change written *before this call* has reached the
73
+ * upstream. Rejects if replication stops with an error.
74
+ *
75
+ * The target is pinned to the outbox position at call time rather than
76
+ * "outbox is empty" — otherwise a concurrent writer could keep the outbox
77
+ * non-empty forever, and, worse, a `drained` event from a scan that began
78
+ * before the caller's own write would resolve it too early.
79
+ */
80
+ waitForDrain(): Promise<void>;
81
+ /** Current replication progress. */
82
+ status(): Promise<SyncStatus>;
83
+ /** Changes permanently rejected upstream. */
84
+ deadLetters(limit?: number): Promise<SyncDeadLetter[]>;
85
+ private pending;
86
+ /** `null` means "every collection"; an array restricts the outbox query. */
87
+ private collectionFilter;
88
+ /**
89
+ * Discovers collections to watch and installs their triggers.
90
+ *
91
+ * Re-run periodically so collections created after `start()` join replication
92
+ * without a restart.
93
+ */
94
+ private refreshCollections;
95
+ /**
96
+ * Seeds the upstream with documents that existed before replication started.
97
+ *
98
+ * The checkpoint is pinned *before* the scan begins, so a write racing the
99
+ * scan is either seen by the scan, or captured in the outbox above the
100
+ * checkpoint, or both — and "both" is harmless because upserts are idempotent.
101
+ */
102
+ private backfill;
103
+ /** The replication loop: read, coalesce, apply, checkpoint, repeat. */
104
+ private run;
105
+ /**
106
+ * Applies one batch.
107
+ * @returns `true` if there was work to do, `false` if the outbox was empty.
108
+ */
109
+ private drainOnce;
110
+ /** Acknowledges up to `id` and prunes rows no replicator needs any more. */
111
+ private advanceTo;
112
+ /**
113
+ * Collapses a run of changes into one operation per document.
114
+ *
115
+ * Records are in ascending outbox order, so the last one seen for a document
116
+ * is its current state — an upsert of the newest revision, or a delete.
117
+ */
118
+ private coalesce;
119
+ /**
120
+ * Turns coalesced records into upstream operations, attaching the base version and
121
+ * minimal diff from the shadow.
122
+ *
123
+ * Shadows load per collection in one query rather than per document, so a batch of
124
+ * 500 changes costs one extra read per collection, not 500.
125
+ */
126
+ private buildOperations;
127
+ /**
128
+ * Records the new upstream state for operations that landed.
129
+ *
130
+ * The post-write state is computed by applying the diff to the previous *server*
131
+ * document rather than by re-reading it — one fewer round trip, and it keeps the BSON
132
+ * values of untouched fields exactly as they were.
133
+ */
134
+ private commitShadows;
135
+ /**
136
+ * Reconciles a push that lost a race.
137
+ *
138
+ * The upstream state goes into the shadow first, so whatever happens next — a retry,
139
+ * or the application's own decision — is made against current reality rather than
140
+ * against the version that already lost.
141
+ */
142
+ private resolveConflict;
143
+ /**
144
+ * Writes the upstream version of a document into the local collection.
145
+ *
146
+ * The write trips the capture triggers and lands back in the outbox, which would
147
+ * normally mean pushing it straight back up. It does not, because the local row now
148
+ * matches the shadow exactly: the diff comes out empty and the echo is dropped in
149
+ * {@link SyncReplicator.buildOperations}. That is why the local copy is written from
150
+ * the same projection the diff compares against.
151
+ */
152
+ private adoptUpstream;
153
+ private fetchUpstream;
154
+ /** Maps an outbox record to an upstream operation, applying rename and transform. */
155
+ private toOperation;
156
+ /**
157
+ * Applies a batch, retrying transient failures with exponential backoff and
158
+ * jitter. Returns the operations the upstream rejected permanently, or `null`
159
+ * if the replicator was stopped before the batch landed.
160
+ *
161
+ * The checkpoint is never advanced while this is retrying, so an upstream
162
+ * outage simply parks the batch — the outbox absorbs the backlog.
163
+ */
164
+ private applyWithRetry;
165
+ private ensureConnected;
166
+ private recordDeadLetter;
167
+ /**
168
+ * Keeps the outbox from growing without bound during a long outage.
169
+ * Compaction is lossless here because replication is full-document
170
+ * last-write-wins: only superseded revisions are dropped.
171
+ */
172
+ private enforceOutboxLimit;
173
+ /** Sleeps, but returns early when `notify()` or `stop()` is called. */
174
+ private sleep;
175
+ private fail;
176
+ private log_;
177
+ }