@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.
Files changed (67) hide show
  1. package/README.md +80 -21
  2. package/dist/cjs/collection.js +9 -6
  3. package/dist/cjs/cursors/findCursor.js +6 -4
  4. package/dist/cjs/index.js +108 -1
  5. package/dist/cjs/mongo-client.js +18 -1
  6. package/dist/cjs/server.js +164 -0
  7. package/dist/cjs/sync/commands.js +126 -0
  8. package/dist/cjs/sync/diff.js +174 -0
  9. package/dist/cjs/sync/httpSink.js +159 -0
  10. package/dist/cjs/sync/index.js +40 -0
  11. package/dist/cjs/sync/mongoExecutor.js +306 -0
  12. package/dist/cjs/sync/mongoSink.js +161 -0
  13. package/dist/cjs/sync/outbox.js +358 -0
  14. package/dist/cjs/sync/protocol.js +177 -0
  15. package/dist/cjs/sync/replicator.js +747 -0
  16. package/dist/cjs/sync/shadow.js +178 -0
  17. package/dist/cjs/sync/types.js +2 -0
  18. package/dist/cloudflare.d.ts +1 -1
  19. package/dist/cloudflare.js.map +1 -1
  20. package/dist/collection.js +9 -6
  21. package/dist/collection.js.map +1 -1
  22. package/dist/cursors/findCursor.d.ts +2 -1
  23. package/dist/cursors/findCursor.js +6 -4
  24. package/dist/cursors/findCursor.js.map +1 -1
  25. package/dist/index.d.ts +68 -2
  26. package/dist/index.js +96 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/mongo-client.d.ts +13 -0
  29. package/dist/mongo-client.js +18 -1
  30. package/dist/mongo-client.js.map +1 -1
  31. package/dist/server.d.ts +97 -0
  32. package/dist/server.js +158 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/sync/commands.d.ts +49 -0
  35. package/dist/sync/commands.js +118 -0
  36. package/dist/sync/commands.js.map +1 -0
  37. package/dist/sync/diff.d.ts +55 -0
  38. package/dist/sync/diff.js +169 -0
  39. package/dist/sync/diff.js.map +1 -0
  40. package/dist/sync/httpSink.d.ts +93 -0
  41. package/dist/sync/httpSink.js +155 -0
  42. package/dist/sync/httpSink.js.map +1 -0
  43. package/dist/sync/index.d.ts +17 -0
  44. package/dist/sync/index.js +10 -0
  45. package/dist/sync/index.js.map +1 -0
  46. package/dist/sync/mongoExecutor.d.ts +37 -0
  47. package/dist/sync/mongoExecutor.js +302 -0
  48. package/dist/sync/mongoExecutor.js.map +1 -0
  49. package/dist/sync/mongoSink.d.ts +109 -0
  50. package/dist/sync/mongoSink.js +123 -0
  51. package/dist/sync/mongoSink.js.map +1 -0
  52. package/dist/sync/outbox.d.ts +120 -0
  53. package/dist/sync/outbox.js +355 -0
  54. package/dist/sync/outbox.js.map +1 -0
  55. package/dist/sync/protocol.d.ts +93 -0
  56. package/dist/sync/protocol.js +168 -0
  57. package/dist/sync/protocol.js.map +1 -0
  58. package/dist/sync/replicator.d.ts +177 -0
  59. package/dist/sync/replicator.js +744 -0
  60. package/dist/sync/replicator.js.map +1 -0
  61. package/dist/sync/shadow.d.ts +54 -0
  62. package/dist/sync/shadow.js +173 -0
  63. package/dist/sync/shadow.js.map +1 -0
  64. package/dist/sync/types.d.ts +388 -0
  65. package/dist/sync/types.js +2 -0
  66. package/dist/sync/types.js.map +1 -0
  67. package/package.json +16 -2
package/dist/server.js ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Server-side entry point: receives sync messages from a MongoLite client and applies
3
+ * them to MongoDB.
4
+ *
5
+ * Install this on the API that sits in front of your database, for deployments where
6
+ * the local instance cannot reach MongoDB directly.
7
+ *
8
+ * ```ts
9
+ * import { MongoClient } from 'mongodb';
10
+ * import { createSyncReceiver } from '@semics-tech/mongolite/server';
11
+ *
12
+ * const mongo = await new MongoClient(process.env.MONGO_URL!).connect();
13
+ * const receiver = createSyncReceiver({
14
+ * client: mongo,
15
+ * database: 'app',
16
+ * allowedCollections: ['users', 'orders'],
17
+ * });
18
+ *
19
+ * app.post('/sync/app/_sync', async (req, res) => {
20
+ * const result = await receiver.apply(req.body);
21
+ * res.status(result.status).type('application/json').send(result.body);
22
+ * });
23
+ * ```
24
+ *
25
+ * This module imports nothing SQLite-related, so it is safe to install on a server that
26
+ * has no local database of its own.
27
+ */
28
+ import { buildWriteCommand } from './sync/commands.js';
29
+ import { executeWrites, fetchDocuments } from './sync/mongoExecutor.js';
30
+ import { SYNC_PROTOCOL_VERSION, SyncProtocolError, decodeBody, encodeBody, parseApplyRequest, parseFetchRequest, } from './sync/protocol.js';
31
+ export { SYNC_PROTOCOL_VERSION, SyncProtocolError } from './sync/protocol.js';
32
+ const JSON_HEADERS = { 'content-type': 'application/json' };
33
+ export class SyncReceiver {
34
+ constructor(options) {
35
+ this.options = options;
36
+ if (!options.client)
37
+ throw new Error('createSyncReceiver requires a connected `client`.');
38
+ this.db = options.client.db(options.database);
39
+ }
40
+ /**
41
+ * Applies a batch of operations.
42
+ *
43
+ * @param body The raw request body — a string, or an already-parsed object.
44
+ * @returns Status and Extended JSON body to send back.
45
+ */
46
+ async apply(body) {
47
+ try {
48
+ const decoded = decodeBody(body);
49
+ const { replicator, operations } = parseApplyRequest(decoded);
50
+ const maxOperations = this.options.maxOperations ?? 1000;
51
+ if (operations.length > maxOperations) {
52
+ throw new SyncProtocolError(`Batch of ${operations.length} exceeds the limit of ${maxOperations}`, 413);
53
+ }
54
+ const collections = [...new Set(operations.map((op) => op.collection))];
55
+ this.assertCollectionsAllowed(collections);
56
+ await this.authorise({ replicator, collections, operationCount: operations.length });
57
+ const writes = operations.map((op, index) => ({
58
+ index,
59
+ op,
60
+ command: this.commandFor(op, decoded, index),
61
+ }));
62
+ const result = await executeWrites(this.db, writes, {
63
+ writeConcern: this.options.writeConcern ?? { w: 'majority' },
64
+ versioning: this.options.versioning ?? true,
65
+ idMapping: this.options.idMapping,
66
+ ObjectId: this.options.ObjectId,
67
+ });
68
+ return this.ok({
69
+ protocol: SYNC_PROTOCOL_VERSION,
70
+ applied: result.applied,
71
+ failures: result.failures,
72
+ conflicts: result.conflicts,
73
+ });
74
+ }
75
+ catch (err) {
76
+ return this.error(err);
77
+ }
78
+ }
79
+ /** Reads documents back by `_id`, so a client can refresh its shadow after a conflict. */
80
+ async fetch(body) {
81
+ try {
82
+ const { collection, documentIds } = parseFetchRequest(decodeBody(body));
83
+ this.assertCollectionsAllowed([collection]);
84
+ await this.authorise({
85
+ replicator: 'fetch',
86
+ collections: [collection],
87
+ operationCount: documentIds.length,
88
+ });
89
+ const documents = await fetchDocuments(this.db, collection, documentIds, this.options.idMapping, this.options.ObjectId);
90
+ return this.ok({ protocol: SYNC_PROTOCOL_VERSION, documents });
91
+ }
92
+ catch (err) {
93
+ return this.error(err);
94
+ }
95
+ }
96
+ // ---------------------------------------------------------------- internals
97
+ /**
98
+ * Produces the command to execute for an operation.
99
+ *
100
+ * By default this **rebuilds** the command from the operation's own fields rather than
101
+ * using whatever the client sent. For a well-behaved client the two are identical;
102
+ * for a hostile one the difference is the whole security boundary.
103
+ */
104
+ commandFor(op, decoded, index) {
105
+ if (this.options.trustClientCommands) {
106
+ const wire = decoded.operations?.[index];
107
+ const command = wire?.command;
108
+ if (command && typeof command === 'object')
109
+ return command;
110
+ }
111
+ return buildWriteCommand(op, {
112
+ idMapping: this.options.idMapping,
113
+ versioning: this.options.versioning ?? true,
114
+ ObjectId: this.options.ObjectId,
115
+ });
116
+ }
117
+ assertCollectionsAllowed(collections) {
118
+ const allowed = this.options.allowedCollections;
119
+ if (!allowed)
120
+ return;
121
+ const permitted = new Set(allowed);
122
+ for (const collection of collections) {
123
+ if (!permitted.has(collection)) {
124
+ throw new SyncProtocolError(`Collection "${collection}" is not permitted`, 403);
125
+ }
126
+ }
127
+ }
128
+ async authorise(context) {
129
+ if (!this.options.verifyRequest)
130
+ return;
131
+ const verdict = await this.options.verifyRequest(context);
132
+ if (verdict === false) {
133
+ throw new SyncProtocolError('Request rejected', 403);
134
+ }
135
+ }
136
+ ok(payload) {
137
+ return { status: 200, body: encodeBody(payload), headers: JSON_HEADERS };
138
+ }
139
+ error(err) {
140
+ const status = err instanceof SyncProtocolError ? err.status : 500;
141
+ const message = err instanceof Error ? err.message : String(err);
142
+ return {
143
+ status,
144
+ // 5xx bodies stay vague: an unexpected error can carry connection strings or
145
+ // document contents, and this response goes to a client.
146
+ body: encodeBody({
147
+ protocol: SYNC_PROTOCOL_VERSION,
148
+ error: status >= 500 ? 'Internal error applying sync batch' : message,
149
+ }),
150
+ headers: JSON_HEADERS,
151
+ };
152
+ }
153
+ }
154
+ /** Creates a receiver for sync messages posted by a MongoLite client. */
155
+ export function createSyncReceiver(options) {
156
+ return new SyncReceiver(options);
157
+ }
158
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { SyncIdMapping, SyncOperation } from './types.js';
2
+ /** Field carrying the optimistic-concurrency version on upstream documents. */
3
+ export declare const VERSION_FIELD = "_v";
4
+ /** Field carrying the server-stamped last-write time on upstream documents. */
5
+ export declare const UPDATED_AT_FIELD = "_updatedAt";
6
+ /** A single entry in a `bulkWrite` array. */
7
+ export type MongoWriteCommand = Record<string, unknown>;
8
+ /** Constructs an upstream `_id` value from MongoLite's string id. */
9
+ export interface ObjectIdFactory {
10
+ new (id: string): unknown;
11
+ isValid(id: string): boolean;
12
+ }
13
+ export interface BuildCommandOptions {
14
+ /** See {@link SyncIdMapping}. Defaults to `'auto'`. */
15
+ idMapping?: SyncIdMapping;
16
+ /** Guard writes with a conditional update against `_v`. Defaults to `true`. */
17
+ versioning?: boolean;
18
+ /**
19
+ * `ObjectId` implementation to use. The direct sink passes the driver's so BSON
20
+ * instances match the connection that will serialise them; the HTTP sink uses the
21
+ * one from `bson`, which Extended JSON encodes as `{"$oid": …}` for the wire.
22
+ */
23
+ ObjectId?: ObjectIdFactory;
24
+ }
25
+ /** True when this operation asserts a specific upstream revision. */
26
+ export declare function isConditional(op: SyncOperation): boolean;
27
+ /** `_id` is the identity; `_v`/`_updatedAt` are managed by the update itself. */
28
+ export declare function isReservedPath(path: string): boolean;
29
+ export declare function stripReservedFields(value: Record<string, unknown>): Record<string, unknown>;
30
+ /** Reads `_v` off an upstream document, tolerating its absence. */
31
+ export declare function readVersion(document: Record<string, unknown> | undefined): number | null;
32
+ /**
33
+ * MongoLite stores `_id` as a string. Converting ObjectId-shaped ids back to real
34
+ * `ObjectId`s keeps replicated documents indistinguishable from ones written by a
35
+ * native MongoDB client.
36
+ */
37
+ export declare function toUpstreamId(documentId: string, idMapping?: SyncIdMapping, ObjectId?: ObjectIdFactory): unknown;
38
+ /**
39
+ * Builds the upstream write for one operation.
40
+ *
41
+ * Every form is idempotent by `_id`, which is what makes at-least-once delivery safe:
42
+ * replaying a batch reproduces the same upstream state.
43
+ *
44
+ * With versioning on, writes are additionally *conditional* on the document still being
45
+ * at the version we last saw. That is the difference between "my change is newer, so it
46
+ * wins" — which cannot detect a lost update — and "I am replacing exactly the revision
47
+ * I read", which can.
48
+ */
49
+ export declare function buildWriteCommand(op: SyncOperation, options?: BuildCommandOptions): MongoWriteCommand;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Builds the MongoDB write commands that realise a {@link SyncOperation}.
3
+ *
4
+ * This lives on its own because three places must agree on it byte for byte: the
5
+ * direct MongoDB sink, the HTTP sink that ships commands to a remote API, and the
6
+ * receiver on the far side that applies them. If any of the three built its own
7
+ * commands they would drift, and the drift would show up as data loss rather than as a
8
+ * test failure.
9
+ */
10
+ import { ObjectId as BsonObjectId } from 'bson';
11
+ /** Field carrying the optimistic-concurrency version on upstream documents. */
12
+ export const VERSION_FIELD = '_v';
13
+ /** Field carrying the server-stamped last-write time on upstream documents. */
14
+ export const UPDATED_AT_FIELD = '_updatedAt';
15
+ /** True when this operation asserts a specific upstream revision. */
16
+ export function isConditional(op) {
17
+ return op.baseVersion !== null && op.baseVersion !== undefined;
18
+ }
19
+ /** `_id` is the identity; `_v`/`_updatedAt` are managed by the update itself. */
20
+ export function isReservedPath(path) {
21
+ const root = path.split('.')[0];
22
+ return root === '_id' || root === VERSION_FIELD || root === UPDATED_AT_FIELD;
23
+ }
24
+ export function stripReservedFields(value) {
25
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !isReservedPath(key)));
26
+ }
27
+ /** Reads `_v` off an upstream document, tolerating its absence. */
28
+ export function readVersion(document) {
29
+ if (!document)
30
+ return null;
31
+ const raw = document[VERSION_FIELD];
32
+ return typeof raw === 'number' ? raw : null;
33
+ }
34
+ /**
35
+ * MongoLite stores `_id` as a string. Converting ObjectId-shaped ids back to real
36
+ * `ObjectId`s keeps replicated documents indistinguishable from ones written by a
37
+ * native MongoDB client.
38
+ */
39
+ export function toUpstreamId(documentId, idMapping = 'auto', ObjectId = BsonObjectId) {
40
+ if (idMapping === 'string')
41
+ return documentId;
42
+ try {
43
+ return ObjectId.isValid(documentId) && /^[0-9a-fA-F]{24}$/.test(documentId)
44
+ ? new ObjectId(documentId)
45
+ : documentId;
46
+ }
47
+ catch {
48
+ return documentId;
49
+ }
50
+ }
51
+ /**
52
+ * Builds the upstream write for one operation.
53
+ *
54
+ * Every form is idempotent by `_id`, which is what makes at-least-once delivery safe:
55
+ * replaying a batch reproduces the same upstream state.
56
+ *
57
+ * With versioning on, writes are additionally *conditional* on the document still being
58
+ * at the version we last saw. That is the difference between "my change is newer, so it
59
+ * wins" — which cannot detect a lost update — and "I am replacing exactly the revision
60
+ * I read", which can.
61
+ */
62
+ export function buildWriteCommand(op, options = {}) {
63
+ const { idMapping = 'auto', versioning = true, ObjectId } = options;
64
+ const _id = toUpstreamId(op.documentId, idMapping, ObjectId);
65
+ if (op.type === 'delete') {
66
+ const filter = isConditional(op) ? { _id, [VERSION_FIELD]: op.baseVersion } : { _id };
67
+ return { deleteOne: { filter } };
68
+ }
69
+ if (!versioning) {
70
+ // The local database is treated as authoritative: replace wholesale, so a field
71
+ // deleted locally disappears upstream too.
72
+ return {
73
+ replaceOne: { filter: { _id }, replacement: { ...(op.document ?? {}), _id }, upsert: true },
74
+ };
75
+ }
76
+ // Never been upstream: create it, and let a duplicate key tell us we lost the race to
77
+ // create. `$setOnInsert` alone means an existing document is left untouched.
78
+ if (!isConditional(op)) {
79
+ const document = stripReservedFields(op.document ?? {});
80
+ return {
81
+ updateOne: {
82
+ filter: { _id },
83
+ update: {
84
+ $setOnInsert: { ...document, [VERSION_FIELD]: 1 },
85
+ $currentDate: { [UPDATED_AT_FIELD]: true },
86
+ },
87
+ upsert: true,
88
+ },
89
+ };
90
+ }
91
+ // Known revision: apply only what changed, conditional on that revision.
92
+ const update = {
93
+ $inc: { [VERSION_FIELD]: 1 },
94
+ $currentDate: { [UPDATED_AT_FIELD]: true },
95
+ };
96
+ const diff = op.diff;
97
+ if (diff) {
98
+ const set = stripReservedFields(diff.set);
99
+ const unset = diff.unset.filter((path) => !isReservedPath(path));
100
+ if (Object.keys(set).length > 0)
101
+ update.$set = set;
102
+ if (unset.length > 0) {
103
+ update.$unset = Object.fromEntries(unset.map((path) => [path, '']));
104
+ }
105
+ }
106
+ else {
107
+ // No shadow to diff against — fall back to writing the whole document, minus the
108
+ // bookkeeping fields the update itself manages.
109
+ update.$set = stripReservedFields(op.document ?? {});
110
+ }
111
+ return {
112
+ updateOne: {
113
+ filter: { _id, [VERSION_FIELD]: op.baseVersion },
114
+ update,
115
+ },
116
+ };
117
+ }
118
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/sync/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Field-level diffing between the last-known server state and the local document.
3
+ *
4
+ * Replication used to push whole documents, which meant a local edit to one field
5
+ * rewrote every other field too — including fields another writer had just changed,
6
+ * and including fields whose BSON type the local store cannot represent. Diffing
7
+ * narrows each push to `$set`/`$unset` of what genuinely changed, so everything else
8
+ * upstream is left exactly as it was.
9
+ */
10
+ /** A minimal update expressed as dotted-path assignments and removals. */
11
+ export interface DocumentDiff {
12
+ /** Dotted path → new value. */
13
+ set: Record<string, unknown>;
14
+ /** Dotted paths to remove. */
15
+ unset: string[];
16
+ }
17
+ /**
18
+ * Structural equality, insensitive to key order.
19
+ *
20
+ * `JSON.stringify` comparison would report `{a:1,b:2}` and `{b:2,a:1}` as different and
21
+ * push a spurious update on every sync.
22
+ */
23
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
24
+ /**
25
+ * Computes the minimal update that turns `base` into `local`.
26
+ *
27
+ * `base` must be the **lossy JSON projection** of the server document — the same shape
28
+ * the local store would have produced for it (see `projectToLocalShape` in `shadow.ts`).
29
+ * That is what makes type preservation work: a server `Date` projects to the same ISO
30
+ * string the local store holds, compares equal, and so never appears in the diff — the
31
+ * upstream `Date` survives untouched.
32
+ *
33
+ * Nested objects recurse into dotted paths, so two writers editing different sub-fields
34
+ * of the same document do not collide.
35
+ *
36
+ * **Arrays are atomic.** Any change to an array replaces the whole array. Positional
37
+ * diffing of arrays is famously error-prone once elements are inserted or reordered, and
38
+ * a wrong answer here silently corrupts data — so this deliberately does not attempt it.
39
+ *
40
+ * @param base The last-known server state, projected to local shape. `null` if the
41
+ * document has never been upstream, in which case every field is new.
42
+ * @param local The current local document.
43
+ */
44
+ export declare function computeDiff(base: Record<string, unknown> | null, local: Record<string, unknown>): DocumentDiff;
45
+ /** True when the diff would change nothing upstream. */
46
+ export declare function isEmptyDiff(diff: DocumentDiff): boolean;
47
+ /**
48
+ * Applies a diff to a copy of `document`, mirroring what the upstream `$set`/`$unset`
49
+ * does.
50
+ *
51
+ * Used to roll the shadow forward after a successful push without a second round trip.
52
+ * Because it edits the *server* document in place, fields the diff never mentions keep
53
+ * their original BSON values — which is the whole point of diffing in the first place.
54
+ */
55
+ export declare function applyDiff(document: Record<string, unknown>, diff: DocumentDiff): Record<string, unknown>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Field-level diffing between the last-known server state and the local document.
3
+ *
4
+ * Replication used to push whole documents, which meant a local edit to one field
5
+ * rewrote every other field too — including fields another writer had just changed,
6
+ * and including fields whose BSON type the local store cannot represent. Diffing
7
+ * narrows each push to `$set`/`$unset` of what genuinely changed, so everything else
8
+ * upstream is left exactly as it was.
9
+ */
10
+ /**
11
+ * True when `value` is a genuine plain object worth recursing into.
12
+ *
13
+ * Class instances are deliberately excluded. A `Date`, `ObjectId` or `Binary` is
14
+ * `typeof 'object'`, and treating one as a bag of fields would copy it into a plain
15
+ * object — destroying exactly the BSON types the shadow exists to preserve.
16
+ */
17
+ function isPlainObject(value) {
18
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
19
+ return false;
20
+ const proto = Object.getPrototypeOf(value);
21
+ return proto === Object.prototype || proto === null;
22
+ }
23
+ /**
24
+ * Structural equality, insensitive to key order.
25
+ *
26
+ * `JSON.stringify` comparison would report `{a:1,b:2}` and `{b:2,a:1}` as different and
27
+ * push a spurious update on every sync.
28
+ */
29
+ export function deepEqual(a, b) {
30
+ if (a === b)
31
+ return true;
32
+ // NaN is the one primitive that isn't equal to itself.
33
+ if (typeof a === 'number' && typeof b === 'number')
34
+ return Number.isNaN(a) && Number.isNaN(b);
35
+ // Two equal instants are separate objects; without this every sync would rewrite
36
+ // any date-valued field.
37
+ if (a instanceof Date || b instanceof Date) {
38
+ return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
39
+ }
40
+ if (Array.isArray(a) || Array.isArray(b)) {
41
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
42
+ return false;
43
+ return a.every((item, index) => deepEqual(item, b[index]));
44
+ }
45
+ if (isPlainObject(a) && isPlainObject(b)) {
46
+ const aKeys = Object.keys(a);
47
+ const bKeys = Object.keys(b);
48
+ if (aKeys.length !== bKeys.length)
49
+ return false;
50
+ return aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]));
51
+ }
52
+ return false;
53
+ }
54
+ /**
55
+ * Computes the minimal update that turns `base` into `local`.
56
+ *
57
+ * `base` must be the **lossy JSON projection** of the server document — the same shape
58
+ * the local store would have produced for it (see `projectToLocalShape` in `shadow.ts`).
59
+ * That is what makes type preservation work: a server `Date` projects to the same ISO
60
+ * string the local store holds, compares equal, and so never appears in the diff — the
61
+ * upstream `Date` survives untouched.
62
+ *
63
+ * Nested objects recurse into dotted paths, so two writers editing different sub-fields
64
+ * of the same document do not collide.
65
+ *
66
+ * **Arrays are atomic.** Any change to an array replaces the whole array. Positional
67
+ * diffing of arrays is famously error-prone once elements are inserted or reordered, and
68
+ * a wrong answer here silently corrupts data — so this deliberately does not attempt it.
69
+ *
70
+ * @param base The last-known server state, projected to local shape. `null` if the
71
+ * document has never been upstream, in which case every field is new.
72
+ * @param local The current local document.
73
+ */
74
+ export function computeDiff(base, local) {
75
+ const diff = { set: {}, unset: [] };
76
+ collect(base ?? {}, local, '', diff);
77
+ return diff;
78
+ }
79
+ function collect(base, local, prefix, diff) {
80
+ const keys = new Set([...Object.keys(base), ...Object.keys(local)]);
81
+ for (const key of keys) {
82
+ // `_id` is the identity, never part of the update.
83
+ if (prefix === '' && key === '_id')
84
+ continue;
85
+ const path = prefix === '' ? key : `${prefix}.${key}`;
86
+ const inLocal = Object.prototype.hasOwnProperty.call(local, key);
87
+ const inBase = Object.prototype.hasOwnProperty.call(base, key);
88
+ if (!inLocal) {
89
+ diff.unset.push(path);
90
+ continue;
91
+ }
92
+ const localValue = local[key];
93
+ const baseValue = base[key];
94
+ if (!inBase) {
95
+ diff.set[path] = localValue;
96
+ continue;
97
+ }
98
+ // Recurse only when both sides are objects; otherwise the shapes differ and the
99
+ // whole subtree is replaced.
100
+ if (isPlainObject(baseValue) && isPlainObject(localValue)) {
101
+ collect(baseValue, localValue, path, diff);
102
+ continue;
103
+ }
104
+ if (!deepEqual(baseValue, localValue)) {
105
+ diff.set[path] = localValue;
106
+ }
107
+ }
108
+ }
109
+ /** True when the diff would change nothing upstream. */
110
+ export function isEmptyDiff(diff) {
111
+ return Object.keys(diff.set).length === 0 && diff.unset.length === 0;
112
+ }
113
+ /**
114
+ * Applies a diff to a copy of `document`, mirroring what the upstream `$set`/`$unset`
115
+ * does.
116
+ *
117
+ * Used to roll the shadow forward after a successful push without a second round trip.
118
+ * Because it edits the *server* document in place, fields the diff never mentions keep
119
+ * their original BSON values — which is the whole point of diffing in the first place.
120
+ */
121
+ export function applyDiff(document, diff) {
122
+ const result = clone(document);
123
+ for (const [path, value] of Object.entries(diff.set)) {
124
+ setPath(result, path.split('.'), value);
125
+ }
126
+ for (const path of diff.unset) {
127
+ unsetPath(result, path.split('.'));
128
+ }
129
+ return result;
130
+ }
131
+ /**
132
+ * Structured clone that leaves class instances (BSON `Date`, `ObjectId`, `Binary`, …)
133
+ * by reference. Copying them field-by-field would destroy exactly the types the shadow
134
+ * exists to preserve.
135
+ */
136
+ function clone(value) {
137
+ if (Array.isArray(value))
138
+ return value.map(clone);
139
+ if (isPlainObject(value)) {
140
+ const out = {};
141
+ for (const [key, item] of Object.entries(value))
142
+ out[key] = clone(item);
143
+ return out;
144
+ }
145
+ return value;
146
+ }
147
+ function setPath(target, segments, value) {
148
+ const [head, ...rest] = segments;
149
+ if (rest.length === 0) {
150
+ target[head] = value;
151
+ return;
152
+ }
153
+ const next = target[head];
154
+ // Mirror MongoDB: a dotted path creates intermediate objects as needed.
155
+ const container = isPlainObject(next) ? next : {};
156
+ target[head] = container;
157
+ setPath(container, rest, value);
158
+ }
159
+ function unsetPath(target, segments) {
160
+ const [head, ...rest] = segments;
161
+ if (rest.length === 0) {
162
+ delete target[head];
163
+ return;
164
+ }
165
+ const next = target[head];
166
+ if (isPlainObject(next))
167
+ unsetPath(next, rest);
168
+ }
169
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/sync/diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACtF,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAY,CAAC;IACtD,OAAO,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC;AACtD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,CAAU,EAAE,CAAU;IAC9C,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzB,uDAAuD;IACvD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE9F,iFAAiF;IACjF,yBAAyB;IACzB,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;QAC3C,OAAO,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;IAC/E,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAClF,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,aAAa,CAAC,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAChD,OAAO,KAAK,CAAC,KAAK,CAChB,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CACnF,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,WAAW,CACzB,IAAoC,EACpC,KAA8B;IAE9B,MAAM,IAAI,GAAiB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAClD,OAAO,CAAC,IAAI,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CACd,IAA6B,EAC7B,KAA8B,EAC9B,MAAc,EACd,IAAkB;IAElB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEpE,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,mDAAmD;QACnD,IAAI,MAAM,KAAK,EAAE,IAAI,GAAG,KAAK,KAAK;YAAE,SAAS;QAE7C,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;QACtD,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAE/D,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QAED,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QAE5B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC;YAC5B,SAAS;QACX,CAAC;QAED,gFAAgF;QAChF,6BAA6B;QAC7B,IAAI,aAAa,CAAC,SAAS,CAAC,IAAI,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1D,OAAO,CAAC,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAC3C,SAAS;QACX,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC;QAC9B,CAAC;IACH,CAAC;AACH,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CACvB,QAAiC,EACjC,IAAkB;IAElB,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAA4B,CAAC;IAE1D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IAC1C,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,SAAS,KAAK,CAAC,KAAc;IAC3B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACxE,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,OAAO,CAAC,MAA+B,EAAE,QAAkB,EAAE,KAAc;IAClF,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAC;IACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACrB,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC1B,wEAAwE;IACxE,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;IACzB,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,SAAS,CAAC,MAA+B,EAAE,QAAkB;IACpE,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAC;IACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,aAAa,CAAC,IAAI,CAAC;QAAE,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC"}