@objy/server 0.0.0-stage → 0.1.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/API.md +1469 -0
- package/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +21 -2
- package/objydb.config.json +48 -0
- package/package.json +42 -4
- package/public/index.html +4134 -0
- package/public/objy-db-logo.png +0 -0
- package/src/aggregate.js +438 -0
- package/src/api.js +15 -0
- package/src/auth.js +472 -0
- package/src/clock.js +18 -0
- package/src/cluster-secret.js +25 -0
- package/src/cluster-status.js +562 -0
- package/src/compression-stats.js +32 -0
- package/src/config.js +360 -0
- package/src/crdt.js +1076 -0
- package/src/derived-state-scheduler.js +128 -0
- package/src/files.js +324 -0
- package/src/hnsw.js +269 -0
- package/src/http-cors.js +31 -0
- package/src/http-router.js +162 -0
- package/src/index.js +137 -0
- package/src/interfaces/auth.js +123 -0
- package/src/interfaces/cluster.js +296 -0
- package/src/interfaces/public-object.js +2452 -0
- package/src/interfaces/ui.js +38 -0
- package/src/lib/crypto.cjs +54 -0
- package/src/lib/jwt.cjs +231 -0
- package/src/local-wal.js +122 -0
- package/src/memory-guard.js +152 -0
- package/src/ndjson.js +117 -0
- package/src/node-runtime.js +29 -0
- package/src/object-hooks.js +282 -0
- package/src/object-id.js +7 -0
- package/src/object-storage.js +263 -0
- package/src/object-store.js +288 -0
- package/src/patch.js +87 -0
- package/src/permissions.js +178 -0
- package/src/persistence.js +1918 -0
- package/src/plugin-loader.js +21 -0
- package/src/query-planner.js +266 -0
- package/src/query.js +416 -0
- package/src/references.js +349 -0
- package/src/replication-scheduler.js +303 -0
- package/src/restore-status.js +77 -0
- package/src/runtime.js +47 -0
- package/src/server.js +242 -0
- package/src/shape.js +286 -0
- package/src/shard-map-store.js +85 -0
- package/src/shard-map.js +333 -0
- package/src/shard-router.js +737 -0
- package/src/store.js +3410 -0
- package/src/system-secrets.js +107 -0
- package/src/vector.js +83 -0
package/src/crdt.js
ADDED
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
import { Store } from "./store.js";
|
|
2
|
+
import { makeClock, nextTs, touch } from "./clock.js";
|
|
3
|
+
import { DEFAULT_SHARD, GLOBAL_COLLECTION, GLOBAL_DATABASE, ShardMap } from "./shard-map.js";
|
|
4
|
+
import { aggregateRows } from "./aggregate.js";
|
|
5
|
+
import { matchesQuery } from "./query.js";
|
|
6
|
+
import {
|
|
7
|
+
baseCriteriaForReferenceQuery,
|
|
8
|
+
expandReferencesSync,
|
|
9
|
+
normalizeRefDepth,
|
|
10
|
+
paginateReferencePairs,
|
|
11
|
+
pathHasReferenceObject,
|
|
12
|
+
referenceDepthForAggregateSpec,
|
|
13
|
+
referenceDepthForQuery,
|
|
14
|
+
referenceReadPlan,
|
|
15
|
+
sortReferencePairs,
|
|
16
|
+
stripReferenceObjects,
|
|
17
|
+
} from "./references.js";
|
|
18
|
+
import { createObjectId } from "./object-id.js";
|
|
19
|
+
import { randomUUID, yieldToEventLoop } from "./runtime.js";
|
|
20
|
+
|
|
21
|
+
export class CRDTNode {
|
|
22
|
+
constructor({ id = randomUUID(), url, clock, retainOps = 256, syncBatchSize = 1024, shardMap, storage, vector, operationLog = true } = {}) {
|
|
23
|
+
this.id = id;
|
|
24
|
+
this.url = normalizeUrl(url);
|
|
25
|
+
this.db = new Store({ ...(storage || {}), vector });
|
|
26
|
+
this.clock = clock || makeClock(id);
|
|
27
|
+
this.log = [];
|
|
28
|
+
this.operationLog = operationLog !== false;
|
|
29
|
+
this.seq = 0;
|
|
30
|
+
this.trimmed = 0;
|
|
31
|
+
this.scopeTrimmed = new Map();
|
|
32
|
+
this.retainOps = retainOps;
|
|
33
|
+
this.lastSeen = new Map();
|
|
34
|
+
this.acks = new Map();
|
|
35
|
+
this.peers = [];
|
|
36
|
+
this.shardMap = new ShardMap(shardMap);
|
|
37
|
+
this.syncBatchSize = syncBatchSize;
|
|
38
|
+
this.changeListeners = new Set();
|
|
39
|
+
this.vectorWarmupEnabled = false;
|
|
40
|
+
this.vectorWarmupScheduled = false;
|
|
41
|
+
this.vectorWarmupPromise = Promise.resolve([]);
|
|
42
|
+
this.logTruncatedHandler = null;
|
|
43
|
+
this.lastSyncFailures = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
nextTs() {
|
|
47
|
+
return nextTs(this.clock);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_logOp(op) {
|
|
51
|
+
op.shard = op.shard || this.shardForObject(op.db, op.coll, op.id);
|
|
52
|
+
op.from = this.id;
|
|
53
|
+
op.seq = ++this.seq;
|
|
54
|
+
if (this.operationLog) this.log.push(op);
|
|
55
|
+
return op;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_logOps(ops) {
|
|
59
|
+
return (ops || []).map((op) => this._logOp(op));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_applyOp(op) {
|
|
63
|
+
return this._applyOps([op]).length === 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_applyOps(ops) {
|
|
67
|
+
return this.db.applyOps((ops || []).filter((op) => {
|
|
68
|
+
return op?.db && op.coll && op.id && (op.kind === "touch" || op.kind === "putObj" || op.kind === "delObj");
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_notifyOps(ops) {
|
|
73
|
+
if (!ops.length) return;
|
|
74
|
+
this._notifyChange({ kind: "ops", ops });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_commitLocalOps(ops) {
|
|
78
|
+
const logged = this._logOps(ops);
|
|
79
|
+
const applied = this._applyOps(logged);
|
|
80
|
+
this._notifyOps(applied);
|
|
81
|
+
if (applied.length) this._scheduleVectorIndexWarmup();
|
|
82
|
+
return { ops: logged, applied };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
applyRemote(fromId, ops, cursorKey = fromId) {
|
|
86
|
+
const incoming = Array.isArray(ops) ? ops : [];
|
|
87
|
+
for (const op of incoming) {
|
|
88
|
+
touch(this.clock, op.ts);
|
|
89
|
+
op.shard = op.shard || this.shardForObject(op.db, op.coll, op.id);
|
|
90
|
+
}
|
|
91
|
+
const applied = this._applyOps(incoming);
|
|
92
|
+
this._notifyOps(applied);
|
|
93
|
+
if (applied.length) this._scheduleVectorIndexWarmup();
|
|
94
|
+
this.lastSeen.set(cursorKey, Math.max(this.lastSeen.get(cursorKey) || 0, incoming.length ? incoming[incoming.length - 1].seq : 0));
|
|
95
|
+
return applied.length;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_write(db, coll, id, entries) {
|
|
99
|
+
return this._writeMany(db, coll, [{ id, value: Object.fromEntries(entries) }]).ops;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_writeObject(db, coll, id, value) {
|
|
103
|
+
return this._writeMany(db, coll, [{ id, value }]).ops[0];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_writeMany(db, coll, items) {
|
|
107
|
+
this.db.assertCollectionWritable(db, coll);
|
|
108
|
+
return this._commitLocalOps((items || []).map(({ id, value }) => ({
|
|
109
|
+
kind: Object.keys(value || {}).length ? "putObj" : "touch",
|
|
110
|
+
db,
|
|
111
|
+
coll,
|
|
112
|
+
id: String(id),
|
|
113
|
+
value: this._prepareWriteValue(db, coll, value),
|
|
114
|
+
ts: this.nextTs(),
|
|
115
|
+
})));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
insert(db, coll, obj) {
|
|
119
|
+
return this.insertMany(db, coll, [obj])[0]._id;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
insertMany(db, coll, objects) {
|
|
123
|
+
const { items } = this._commitInsertMany(db, coll, objects);
|
|
124
|
+
return items.map(({ id }) => this.get(db, coll, id) || { _id: id });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
insertManyCount(db, coll, objects) {
|
|
128
|
+
return this._commitInsertMany(db, coll, objects).applied.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_commitInsertMany(db, coll, objects) {
|
|
132
|
+
const items = normalizeInsertObjects(objects);
|
|
133
|
+
assertInsertTargetsMissing(this.db.collection(db, coll), items);
|
|
134
|
+
return { items, ...this._writeMany(db, coll, items) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
update(db, coll, id, obj) {
|
|
138
|
+
return this.updateMany(db, coll, [{ ...withoutObjectId(obj), _id: id }]).items[0]?._id || id;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
replace(db, coll, id, obj) {
|
|
142
|
+
return this.replaceMany(db, coll, [{ ...withoutObjectId(obj), _id: id }]).items[0]?._id || id;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
remove(db, coll, id) {
|
|
146
|
+
return this.removeMany(db, coll, [id]).deleted === 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
replaceMany(db, coll, objects) {
|
|
150
|
+
const items = normalizeObjectsWithIds(objects);
|
|
151
|
+
this._writeMany(db, coll, items.map((object) => ({
|
|
152
|
+
id: object._id,
|
|
153
|
+
value: this.db.preserveVector(db, coll, object._id, withoutObjectId(object)),
|
|
154
|
+
})));
|
|
155
|
+
return {
|
|
156
|
+
replaced: items.length,
|
|
157
|
+
items: this.getMany(db, coll, items.map((item) => item._id)),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
replaceManyCount(db, coll, objects) {
|
|
162
|
+
const items = normalizeObjectsWithIds(objects);
|
|
163
|
+
this._writeMany(db, coll, items.map((object) => ({
|
|
164
|
+
id: object._id,
|
|
165
|
+
value: this.db.preserveVector(db, coll, object._id, withoutObjectId(object)),
|
|
166
|
+
})));
|
|
167
|
+
return items.length;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
updateMany(db, coll, objects) {
|
|
171
|
+
const items = normalizeObjectsWithIds(objects).map((object) => {
|
|
172
|
+
const current = this.get(db, coll, object._id) || { _id: object._id };
|
|
173
|
+
return {
|
|
174
|
+
_id: object._id,
|
|
175
|
+
value: this.db.preserveVector(db, coll, object._id, { ...withoutObjectId(current), ...withoutObjectId(object) }),
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
this._writeMany(db, coll, items.map((item) => ({ id: item._id, value: item.value })));
|
|
179
|
+
return {
|
|
180
|
+
updated: items.length,
|
|
181
|
+
items: this.getMany(db, coll, items.map((item) => item._id)),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
removeMany(db, coll, ids) {
|
|
186
|
+
const skipped = [];
|
|
187
|
+
const ops = [];
|
|
188
|
+
const normalized = normalizeIds(ids);
|
|
189
|
+
for (const id of normalized) {
|
|
190
|
+
if (!this.db.getCollection(db, coll)?.has(id)) {
|
|
191
|
+
skipped.push(id);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
ops.push({ kind: "delObj", db, coll, id, ts: this.nextTs() });
|
|
195
|
+
}
|
|
196
|
+
const { applied } = this._commitLocalOps(ops);
|
|
197
|
+
return {
|
|
198
|
+
deleted: applied.length,
|
|
199
|
+
ids: applied.map((op) => op.id),
|
|
200
|
+
skipped,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get(db, coll, id, options = {}) {
|
|
205
|
+
const object = this.db.getCollection(db, coll)?.read(id);
|
|
206
|
+
if (!object) return object;
|
|
207
|
+
const depth = normalizeRefDepth(options.refDepth, 0);
|
|
208
|
+
return depth > 0 ? this.expandReferences(db, object, depth) : object;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getMany(db, coll, ids, options = {}) {
|
|
212
|
+
const objects = this.db.readObjects(db, coll, ids);
|
|
213
|
+
const depth = normalizeRefDepth(options.refDepth, 0);
|
|
214
|
+
return depth > 0 ? objects.map((object) => this.expandReferences(db, object, depth)) : objects;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
list(db, coll) {
|
|
218
|
+
return this.db.getCollection(db, coll)?.list() || [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
find(db, coll, query = {}, options = {}) {
|
|
222
|
+
if (this._needsReferenceRead(query, options)) return this._findWithReferences(db, coll, query, options);
|
|
223
|
+
return this.db.getCollection(db, coll)?.find(query, options) || [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
findShards(db, coll, query = {}, options = {}, shards = []) {
|
|
227
|
+
const includeId = this._includeShardId(db, coll, shards);
|
|
228
|
+
return this.find(db, coll, query, includeId ? { ...options, includeId } : options);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
queryPath(db, coll, query = {}, options = {}) {
|
|
232
|
+
return this.db.queryPath(db, coll, query, options);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
queryPathShards(db, coll, query = {}, options = {}, shards = []) {
|
|
236
|
+
const includeId = this._includeShardId(db, coll, shards);
|
|
237
|
+
return this.queryPath(db, coll, query, includeId ? { ...options, includeId } : options);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
count(db, coll, query = {}, options = {}) {
|
|
241
|
+
if (referenceDepthForQuery(query) > 0) return this._countWithReferences(db, coll, query, options);
|
|
242
|
+
return this.db.getCollection(db, coll)?.count(query, options) || 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
countShards(db, coll, query = {}, shards = []) {
|
|
246
|
+
return this.count(db, coll, query, this._shardQueryOptions(db, coll, shards));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
aggregate(db, coll, query = {}, spec = {}, options = {}) {
|
|
250
|
+
if (referenceDepthForQuery(query) > 0 || referenceDepthForAggregateSpec(spec) > 0) {
|
|
251
|
+
return this._aggregateWithReferences(db, coll, query, spec);
|
|
252
|
+
}
|
|
253
|
+
return this.db.getCollection(db, coll)?.aggregate(query, spec, options) || [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
aggregateShards(db, coll, query = {}, spec = {}, shards = []) {
|
|
257
|
+
return this.aggregate(db, coll, query, spec, this._shardQueryOptions(db, coll, shards));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
aggregatePath(db, coll, query = {}, spec = {}, options = {}) {
|
|
261
|
+
return this.db.aggregatePath(db, coll, query, spec, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
aggregatePathShards(db, coll, query = {}, spec = {}, shards = []) {
|
|
265
|
+
return this.aggregatePath(db, coll, query, spec, this._shardQueryOptions(db, coll, shards));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
expandReferences(database, object, depth = 1) {
|
|
269
|
+
return expandReferencesSync(object, {
|
|
270
|
+
database,
|
|
271
|
+
depth,
|
|
272
|
+
getObject: (targetDatabase, targetCollection, id) => this.get(targetDatabase, targetCollection, id, { refDepth: 0 }),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
createIndex(db, coll, path) {
|
|
277
|
+
const result = this.db.collection(db, coll).createIndex(path);
|
|
278
|
+
this._notifyCollectionMeta(db, coll);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
dropIndex(db, coll, path) {
|
|
283
|
+
const result = this.db.collection(db, coll).dropIndex(path);
|
|
284
|
+
this._notifyCollectionMeta(db, coll);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
getCollectionMeta(db, coll) {
|
|
289
|
+
return this.db.getCollectionMeta(db, coll);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
createDatabase(db, meta = {}) {
|
|
293
|
+
const next = this.db.createDatabase(db, meta);
|
|
294
|
+
this._notifyChange({ kind: "database", database: db, meta: next });
|
|
295
|
+
return next;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getDatabaseMeta(db) {
|
|
299
|
+
return this.db.getDatabaseMeta(db);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
removeDatabase(db) {
|
|
303
|
+
const alreadyDeleted = this.db.isDatabaseDeleted(db);
|
|
304
|
+
const removed = this.db.deleteDatabase(db);
|
|
305
|
+
if (removed || !alreadyDeleted) {
|
|
306
|
+
this._discardDatabaseOps(db);
|
|
307
|
+
this._notifyChange({ kind: "dropDatabase", database: db });
|
|
308
|
+
}
|
|
309
|
+
return removed;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
createCollection(db, coll, meta = {}) {
|
|
313
|
+
const next = this.db.createCollection(db, coll, meta);
|
|
314
|
+
this._notifyChange({ kind: "meta", database: db, collection: coll, meta: next });
|
|
315
|
+
return next;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setCollectionMeta(db, coll, meta) {
|
|
319
|
+
const next = this.db.setCollectionMeta(db, coll, meta);
|
|
320
|
+
this._notifyChange({ kind: "meta", database: db, collection: coll, meta: next });
|
|
321
|
+
return next;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
removeCollection(db, coll) {
|
|
325
|
+
const alreadyDeleted = this.db.isCollectionDeleted(db, coll);
|
|
326
|
+
const removed = this.db.deleteCollection(db, coll);
|
|
327
|
+
if (removed || !alreadyDeleted) {
|
|
328
|
+
this._discardCollectionOps(db, coll);
|
|
329
|
+
this._notifyChange({ kind: "dropCollection", database: db, collection: coll });
|
|
330
|
+
}
|
|
331
|
+
return removed;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
listDatabases() {
|
|
335
|
+
return this.db.dbNames();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
listCollections(db) {
|
|
339
|
+
return this.db.collectionNames(db);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
stats(db, coll) {
|
|
343
|
+
return this.db.getCollection(db, coll)?.shapeStats() || [];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
indexStats(db, coll) {
|
|
347
|
+
return this.db.getCollection(db, coll)?.indexStats() || [];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
indexReadiness() {
|
|
351
|
+
return this.db.indexReadiness();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
indexSummary() {
|
|
355
|
+
return this.db.indexSummary();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
compressionStats() {
|
|
359
|
+
return this.db.compressionStats();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
compressionSummary() {
|
|
363
|
+
return this.db.compressionSummary();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
awaitColumns(db, coll) {
|
|
367
|
+
return this.db.awaitColumns(db, coll);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
awaitFieldIndexes(db, coll) {
|
|
371
|
+
return this.db.awaitFieldIndexes(db, coll);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
awaitIndexes(db, coll) {
|
|
375
|
+
return this.db.awaitIndexes(db, coll);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
startIndexWarmup() {
|
|
379
|
+
return this.db.startIndexWarmup();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
vectorSearch(db, coll, queryVec, limit = 20, options = {}) {
|
|
383
|
+
return this.db.vectorSearch(db, coll, queryVec, limit, options);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
vectorSearchShards(db, coll, queryVec, limit = 20, shards = [], options = {}) {
|
|
387
|
+
const includeId = this._includeShardId(db, coll, shards);
|
|
388
|
+
return this.vectorSearch(db, coll, queryVec, limit, includeId
|
|
389
|
+
? { includeId, indexKey: vectorShardIndexKey(shards), ...options }
|
|
390
|
+
: options);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
assertVectorIndexReady(db, coll, options = {}) {
|
|
394
|
+
return this.db.assertVectorIndexReady(db, coll, options);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
assertVectorIndexReadyShards(db, coll, shards = []) {
|
|
398
|
+
const includeId = this._includeShardId(db, coll, shards);
|
|
399
|
+
return this.assertVectorIndexReady(db, coll, includeId
|
|
400
|
+
? { includeId, indexKey: vectorShardIndexKey(shards) }
|
|
401
|
+
: {});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
rankVectorRows(db, coll, rows, queryVec, limit = 20) {
|
|
405
|
+
return this.db.rankVectorRows(db, coll, rows, queryVec, limit);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
getVector(db, coll, id) {
|
|
409
|
+
return this.db.getVector(db, coll, id);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
vectorStats() {
|
|
413
|
+
return this.db.vectorStats();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
vectorSummary() {
|
|
417
|
+
return this.db.vectorSummary();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
startVectorIndexWarmup() {
|
|
421
|
+
this.vectorWarmupEnabled = true;
|
|
422
|
+
this.vectorWarmupPromise = this.db.warmVectorIndexes((database, collection) => this._vectorWarmupPlans(database, collection));
|
|
423
|
+
return this.vectorWarmupPromise;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async awaitVectorIndexes() {
|
|
427
|
+
this.vectorWarmupEnabled = true;
|
|
428
|
+
await this.startVectorIndexWarmup();
|
|
429
|
+
await this.db.awaitVectorIndexes();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async syncPeer(peer, scope) {
|
|
433
|
+
let total = 0;
|
|
434
|
+
|
|
435
|
+
for (;;) {
|
|
436
|
+
const key = this._peerKey(peer);
|
|
437
|
+
const cursorKey = this._cursorKey(key, scope);
|
|
438
|
+
const after = this.lastSeen.get(cursorKey) || 0;
|
|
439
|
+
const res = await peer.pull(after, this.syncBatchSize, scope);
|
|
440
|
+
if (!res) return total;
|
|
441
|
+
|
|
442
|
+
if (res.status === 410) {
|
|
443
|
+
const remoteId = res.nodeId || res.node || peer.nodeId;
|
|
444
|
+
if (remoteId) this._bindPeer(peer, remoteId, key);
|
|
445
|
+
const remoteKey = this._cursorKey(remoteId || key, scope);
|
|
446
|
+
const before = this.lastSeen.get(remoteKey) || after;
|
|
447
|
+
const restored = await this._handleLogTruncated({ peer, scope, after, response: res, remoteId });
|
|
448
|
+
let seen = this.lastSeen.get(remoteKey) || 0;
|
|
449
|
+
const restoredRows = Number(restored?.applied || restored?.snapshotObjects || restored?.journalOps || 0);
|
|
450
|
+
const durableFloor = Math.max(0, Number(res.trimmed) || 0);
|
|
451
|
+
if (restoredRows > 0 && durableFloor > seen) {
|
|
452
|
+
this.lastSeen.set(remoteKey, durableFloor);
|
|
453
|
+
seen = durableFloor;
|
|
454
|
+
}
|
|
455
|
+
if (!restored || seen <= before) {
|
|
456
|
+
const err = new Error("replication log truncated and durable catch-up did not advance");
|
|
457
|
+
err.code = "LOG_TRUNCATED";
|
|
458
|
+
err.scope = scope;
|
|
459
|
+
err.peer = peer;
|
|
460
|
+
err.response = res;
|
|
461
|
+
throw err;
|
|
462
|
+
}
|
|
463
|
+
total += Number(restored.applied || restored.snapshotObjects || restored.journalOps || 0);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const remoteId = res.nodeId || res.node || peer.nodeId;
|
|
468
|
+
if (!remoteId || remoteId === this.id) return total;
|
|
469
|
+
|
|
470
|
+
this._bindPeer(peer, remoteId, key);
|
|
471
|
+
const remoteKey = this._cursorKey(remoteId, scope);
|
|
472
|
+
let seen = after;
|
|
473
|
+
let received = 0;
|
|
474
|
+
|
|
475
|
+
if (res.opBatches) {
|
|
476
|
+
for await (const ops of res.opBatches) {
|
|
477
|
+
if (!Array.isArray(ops) || !ops.length) continue;
|
|
478
|
+
received += ops.length;
|
|
479
|
+
total += this.applyRemote(remoteId, ops, remoteKey);
|
|
480
|
+
seen = ops.length ? ops[ops.length - 1].seq : seen;
|
|
481
|
+
this.lastSeen.set(remoteKey, Math.max(this.lastSeen.get(remoteKey) || 0, seen));
|
|
482
|
+
await this._ack(peer, seen, scope);
|
|
483
|
+
}
|
|
484
|
+
if (!received) {
|
|
485
|
+
seen = res.lastSeq || after;
|
|
486
|
+
this.lastSeen.set(remoteKey, Math.max(this.lastSeen.get(remoteKey) || 0, seen));
|
|
487
|
+
await this._ack(peer, seen, scope);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
const ops = Array.isArray(res.ops) ? res.ops : [];
|
|
491
|
+
received = ops.length;
|
|
492
|
+
total += this.applyRemote(remoteId, ops, remoteKey);
|
|
493
|
+
seen = ops.length ? ops[ops.length - 1].seq : res.lastSeq || after;
|
|
494
|
+
this.lastSeen.set(remoteKey, Math.max(this.lastSeen.get(remoteKey) || 0, seen));
|
|
495
|
+
await this._ack(peer, seen, scope);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!received || seen >= (res.lastSeq || seen)) return total;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
setLogTruncatedHandler(handler) {
|
|
503
|
+
this.logTruncatedHandler = typeof handler === "function" ? handler : null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async _handleLogTruncated(event) {
|
|
507
|
+
if (!this.logTruncatedHandler) return null;
|
|
508
|
+
return this.logTruncatedHandler(event);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async _ack(peer, lastSeq, scope) {
|
|
512
|
+
if (!peer.ack) return;
|
|
513
|
+
try {
|
|
514
|
+
await peer.ack(this.id, lastSeq, scope);
|
|
515
|
+
} catch {
|
|
516
|
+
// ack is best-effort; unpruned ops are safe
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
applySnapshot(fromId, snap, cursorKey = fromId) {
|
|
521
|
+
const batches = new Map();
|
|
522
|
+
for (const c of snap.collections || []) {
|
|
523
|
+
for (const obj of c.objects || []) {
|
|
524
|
+
const shard = obj.shard || c.shard || this.shardForObject(c.db, c.coll, obj.id);
|
|
525
|
+
if (obj.deleted) {
|
|
526
|
+
if (obj.ts) touch(this.clock, obj.ts);
|
|
527
|
+
touch(this.clock, obj.deleted);
|
|
528
|
+
} else if (obj.ts) {
|
|
529
|
+
touch(this.clock, obj.ts);
|
|
530
|
+
}
|
|
531
|
+
const key = `${c.db}\0${c.coll}`;
|
|
532
|
+
let batch = batches.get(key);
|
|
533
|
+
if (!batch) {
|
|
534
|
+
batch = { db: c.db, coll: c.coll, objects: [] };
|
|
535
|
+
batches.set(key, batch);
|
|
536
|
+
}
|
|
537
|
+
batch.objects.push({ ...obj, shard });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
let count = 0;
|
|
541
|
+
for (const batch of batches.values()) {
|
|
542
|
+
count += this.db.applySnapshotObjects(batch.db, batch.coll, batch.objects, { trustedValues: true });
|
|
543
|
+
}
|
|
544
|
+
this.lastSeen.set(cursorKey, snap.lastSeq || 0);
|
|
545
|
+
if (count) this._notifyChange({ kind: "snapshot", snapshot: snap });
|
|
546
|
+
return count;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
applySnapshotObjects(fromId, { database, collection, objects, lastSeq = 0, scope } = {}, cursorKey = fromId) {
|
|
550
|
+
if (!database || !collection) return 0;
|
|
551
|
+
const rows = Array.isArray(objects) ? objects : [];
|
|
552
|
+
for (const obj of rows) {
|
|
553
|
+
const shard = obj.shard || scope?.shard || this.shardForObject(database, collection, obj.id);
|
|
554
|
+
if (!obj.shard) obj.shard = shard;
|
|
555
|
+
if (obj.deleted) {
|
|
556
|
+
if (obj.ts) touch(this.clock, obj.ts);
|
|
557
|
+
touch(this.clock, obj.deleted);
|
|
558
|
+
} else if (obj.ts) {
|
|
559
|
+
touch(this.clock, obj.ts);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const count = this.db.applySnapshotObjects(database, collection, rows, { trustedValues: true });
|
|
563
|
+
this.lastSeen.set(cursorKey, lastSeq || 0);
|
|
564
|
+
if (count) this._notifyChange({
|
|
565
|
+
kind: "snapshot",
|
|
566
|
+
snapshot: {
|
|
567
|
+
node: fromId,
|
|
568
|
+
lastSeq,
|
|
569
|
+
collections: [{ db: database, coll: collection, objects: rows }],
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
return count;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
onChange(listener) {
|
|
576
|
+
this.changeListeners.add(listener);
|
|
577
|
+
return () => this.changeListeners.delete(listener);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
_notifyChange(change) {
|
|
581
|
+
for (const listener of this.changeListeners) {
|
|
582
|
+
try {
|
|
583
|
+
listener(change);
|
|
584
|
+
} catch {
|
|
585
|
+
// durability listeners should never break the mutation path
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_notifyCollectionMeta(db, coll) {
|
|
591
|
+
const meta = this.getCollectionMeta(db, coll);
|
|
592
|
+
if (meta) this._notifyChange({ kind: "meta", database: db, collection: coll, meta });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
snapshot(scope) {
|
|
596
|
+
return {
|
|
597
|
+
node: this.id,
|
|
598
|
+
lastSeq: this.seq,
|
|
599
|
+
collections: this._snapshotCollections(scope),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
*snapshotScopeObjects(scope) {
|
|
604
|
+
if (!scope || scope.database === GLOBAL_DATABASE || scope.collection === GLOBAL_COLLECTION) {
|
|
605
|
+
for (const collection of this._snapshotCollections(scope)) {
|
|
606
|
+
yield* collection.objects;
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
yield* this.db.collectionSnapshotObjects(scope.database, scope.collection, {
|
|
612
|
+
includeId: (id) => this.shardForObject(scope.database, scope.collection, id) === scope.shard,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
receiveAck(peerNodeId, after, scope) {
|
|
617
|
+
this.acks.set(this._cursorKey(peerNodeId, scope), Math.min(after, this.seq));
|
|
618
|
+
return this.prune();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
needsSnapshot(after, scope) {
|
|
622
|
+
if (!scope) return after < this.trimmed;
|
|
623
|
+
for (const [key, trimmed] of this.scopeTrimmed) {
|
|
624
|
+
if (trimmed > after && scopeMatches(parseScopeKey(key), scope)) return true;
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
prune() {
|
|
630
|
+
if (!this.acks.size) return 0;
|
|
631
|
+
let watermark = Infinity;
|
|
632
|
+
for (const a of this.acks.values()) if (a < watermark) watermark = a;
|
|
633
|
+
const safe = Math.max(0, watermark - this.retainOps);
|
|
634
|
+
if (safe <= this.trimmed) return 0;
|
|
635
|
+
const cut = Math.min(safe, this.seq);
|
|
636
|
+
const removed = cut - this.trimmed;
|
|
637
|
+
if (removed > 0) {
|
|
638
|
+
const removedOps = [];
|
|
639
|
+
this.log = this.log.filter((op) => {
|
|
640
|
+
if (op.seq > cut) return true;
|
|
641
|
+
removedOps.push(op);
|
|
642
|
+
return false;
|
|
643
|
+
});
|
|
644
|
+
for (const op of removedOps) this._rememberScopeTrim(op);
|
|
645
|
+
this.trimmed = cut;
|
|
646
|
+
}
|
|
647
|
+
return removed;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
_discardCollectionOps(db, coll) {
|
|
651
|
+
return this._discardOps((op) => op.db === db && op.coll === coll);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
_discardDatabaseOps(db) {
|
|
655
|
+
return this._discardOps((op) => op.db === db);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
_discardOps(matches) {
|
|
659
|
+
const removed = [];
|
|
660
|
+
this.log = this.log.filter((op) => {
|
|
661
|
+
if (!matches(op)) return true;
|
|
662
|
+
removed.push(op);
|
|
663
|
+
return false;
|
|
664
|
+
});
|
|
665
|
+
for (const op of removed) this._rememberScopeTrim(op);
|
|
666
|
+
return removed.length;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
_rememberScopeTrim(op) {
|
|
670
|
+
if (!op?.db || !op.coll || !op.shard || !Number.isFinite(op.seq)) return;
|
|
671
|
+
const key = scopeKey({ database: op.db, collection: op.coll, shard: op.shard });
|
|
672
|
+
this.scopeTrimmed.set(key, Math.max(this.scopeTrimmed.get(key) || 0, op.seq));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
getStats() {
|
|
676
|
+
return {
|
|
677
|
+
node: this.id,
|
|
678
|
+
logLength: this.log.length,
|
|
679
|
+
trimmed: this.trimmed,
|
|
680
|
+
retainOps: this.retainOps,
|
|
681
|
+
syncBatchSize: this.syncBatchSize,
|
|
682
|
+
acks: [...this.acks.entries()],
|
|
683
|
+
lastSeen: [...this.lastSeen.entries()],
|
|
684
|
+
scopeTrimmed: [...this.scopeTrimmed.entries()],
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async syncAll() {
|
|
689
|
+
let total = 0;
|
|
690
|
+
const failures = [];
|
|
691
|
+
for (const target of this.ownedShardTargets()) {
|
|
692
|
+
const scope = this._scope(target.database, target.collection, target.shard);
|
|
693
|
+
for (const peer of this._syncPeersForTarget(target)) {
|
|
694
|
+
try {
|
|
695
|
+
total += await this.syncPeer(peer, scope);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
// peer unreachable; will retry on next sync
|
|
698
|
+
failures.push({
|
|
699
|
+
peer: this._peerKey(peer),
|
|
700
|
+
scope,
|
|
701
|
+
message: err?.message || String(err),
|
|
702
|
+
code: err?.code,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
this.lastSyncFailures = failures;
|
|
708
|
+
return total;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
syncScopeForShard(shard = DEFAULT_SHARD) {
|
|
712
|
+
return this._scope(GLOBAL_DATABASE, GLOBAL_COLLECTION, shard);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
ownedShardTargets() {
|
|
716
|
+
const targets = this.shardMap.syncTargets();
|
|
717
|
+
if (!this.url) return targets;
|
|
718
|
+
return targets.filter((target) => target.peers.some((peer) => this._isSelfPeer(peer)));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
addPeer(peer) {
|
|
722
|
+
return this.addPeerToShard({
|
|
723
|
+
shard: DEFAULT_SHARD,
|
|
724
|
+
peer,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
addPeerToShard({ shard = DEFAULT_SHARD, peer }) {
|
|
729
|
+
const added = this.shardMap.addPeer(shard, peer);
|
|
730
|
+
this.peers = this.shardMap.allPeers();
|
|
731
|
+
return added;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
removePeerFromShard({ shard = DEFAULT_SHARD, peer }) {
|
|
735
|
+
const removed = this.shardMap.removePeer(shard, peer);
|
|
736
|
+
this.peers = this.shardMap.allPeers();
|
|
737
|
+
return removed;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
peersForShard(shard = DEFAULT_SHARD) {
|
|
741
|
+
return this.shardMap.peersFor(shard);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
ownsShard(shard = DEFAULT_SHARD) {
|
|
745
|
+
if (!this.url) return true;
|
|
746
|
+
const peers = this.peersForShard(shard);
|
|
747
|
+
if (!peers.length) return true;
|
|
748
|
+
return peers.some((peer) => this._isSelfPeer(peer));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
isPrimaryForShard(shard = DEFAULT_SHARD) {
|
|
752
|
+
if (!this.url) return true;
|
|
753
|
+
const peers = this.peersForShard(shard);
|
|
754
|
+
return !peers.length || this._isSelfPeer(peers[0]);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
routingPeersForShard(shard = DEFAULT_SHARD) {
|
|
758
|
+
return this.peersForShard(shard).filter((peer) => peer.url && !this._isSelfPeer(peer));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
replaceShardMap(shardMap) {
|
|
762
|
+
this.shardMap.replace(shardMap);
|
|
763
|
+
this.peers = this.shardMap.allPeers();
|
|
764
|
+
this.db.clearVectorIndexes();
|
|
765
|
+
this._scheduleVectorIndexWarmup();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
shardMapSnapshot() {
|
|
769
|
+
return this.shardMap.snapshot();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
shardForObject(database, collection, objectId) {
|
|
773
|
+
return this.shardMap.shardForObject(database, collection, objectId);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
_includeShardId(database, collection, shards = []) {
|
|
777
|
+
const shardSet = new Set((shards || []).filter(Boolean));
|
|
778
|
+
if (!shardSet.size) return null;
|
|
779
|
+
return (id) => shardSet.has(this.shardForObject(database, collection, id));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
_shardQueryOptions(database, collection, shards = []) {
|
|
783
|
+
const shardSet = new Set((shards || []).filter(Boolean));
|
|
784
|
+
if (!shardSet.size) return {};
|
|
785
|
+
const scopeShards = [...shardSet];
|
|
786
|
+
const includeIds = this.db.idsForShards(database, collection, [...shardSet]);
|
|
787
|
+
if (includeIds) return { includeIds, scopeShards };
|
|
788
|
+
const includeId = this._includeShardId(database, collection, [...shardSet]);
|
|
789
|
+
return includeId ? { includeId, scopeShards } : {};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
_scheduleVectorIndexWarmup() {
|
|
793
|
+
if (!this.vectorWarmupEnabled || this.vectorWarmupScheduled) return;
|
|
794
|
+
this.vectorWarmupScheduled = true;
|
|
795
|
+
yieldToEventLoop().then(() => {
|
|
796
|
+
this.vectorWarmupScheduled = false;
|
|
797
|
+
this.startVectorIndexWarmup().catch(() => {});
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
_vectorWarmupPlans(database, collection) {
|
|
802
|
+
const shards = this.shardMap.shardsForCollection(database, collection).filter((shard) => this.ownsShard(shard));
|
|
803
|
+
const plans = this.url ? [] : [{ key: "all" }];
|
|
804
|
+
if (!shards.length) return plans.length ? plans : [{ key: "all" }];
|
|
805
|
+
plans.push({
|
|
806
|
+
key: vectorShardIndexKey(shards),
|
|
807
|
+
includeId: this._includeShardId(database, collection, shards),
|
|
808
|
+
});
|
|
809
|
+
for (const shard of shards) {
|
|
810
|
+
plans.push({
|
|
811
|
+
key: vectorShardIndexKey([shard]),
|
|
812
|
+
includeId: this._includeShardId(database, collection, [shard]),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
return plans;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
_needsReferenceRead(query = {}, options = {}) {
|
|
819
|
+
return referenceDepthForQuery(query) > 0 ||
|
|
820
|
+
pathHasReferenceObject(options.sortBy) ||
|
|
821
|
+
normalizeRefDepth(options.refDepth, 0) > 0;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
_findWithReferences(db, coll, query = {}, options = {}) {
|
|
825
|
+
const criteria = query || {};
|
|
826
|
+
const { matchDepth, responseDepth } = referenceReadPlan(criteria, options);
|
|
827
|
+
const collection = this.db.getCollection(db, coll);
|
|
828
|
+
if (!collection) return [];
|
|
829
|
+
if (matchDepth <= 0) {
|
|
830
|
+
return collection
|
|
831
|
+
.find(criteria, withoutReferenceOptions(options))
|
|
832
|
+
.map((object) => this.expandReferences(db, object, responseDepth));
|
|
833
|
+
}
|
|
834
|
+
const fetchOptions = {
|
|
835
|
+
...(typeof options.includeId === "function" ? { includeId: options.includeId } : {}),
|
|
836
|
+
};
|
|
837
|
+
const candidates = collection.find(
|
|
838
|
+
matchDepth > 0 ? baseCriteriaForReferenceQuery(criteria) : criteria,
|
|
839
|
+
fetchOptions,
|
|
840
|
+
);
|
|
841
|
+
const matched = [];
|
|
842
|
+
for (const object of candidates) {
|
|
843
|
+
const matchObject = matchDepth > 0 ? this.expandReferences(db, object, matchDepth) : object;
|
|
844
|
+
if (matchesQuery(matchObject, criteria)) matched.push({ raw: object, matchObject });
|
|
845
|
+
}
|
|
846
|
+
const sorted = sortReferencePairs(matched, options);
|
|
847
|
+
const paged = paginateReferencePairs(sorted, options);
|
|
848
|
+
return paged.map(({ raw }) => responseDepth > 0 ? this.expandReferences(db, raw, responseDepth) : raw);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
_countWithReferences(db, coll, query = {}, options = {}) {
|
|
852
|
+
const criteria = query || {};
|
|
853
|
+
const depth = referenceDepthForQuery(criteria);
|
|
854
|
+
let n = 0;
|
|
855
|
+
const collection = this.db.getCollection(db, coll);
|
|
856
|
+
if (!collection) return 0;
|
|
857
|
+
const candidates = collection.find(baseCriteriaForReferenceQuery(criteria), {
|
|
858
|
+
...(typeof options.includeId === "function" ? { includeId: options.includeId } : {}),
|
|
859
|
+
});
|
|
860
|
+
for (const object of candidates) {
|
|
861
|
+
if (matchesQuery(this.expandReferences(db, object, depth), criteria)) n++;
|
|
862
|
+
}
|
|
863
|
+
return n;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
_aggregateWithReferences(db, coll, query = {}, spec = {}) {
|
|
867
|
+
const depth = Math.max(referenceDepthForQuery(query), referenceDepthForAggregateSpec(spec));
|
|
868
|
+
const rows = this._findWithReferences(db, coll, query, { refDepth: depth });
|
|
869
|
+
return aggregateRows(rows, spec);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
_prepareWriteValue(db, coll, value) {
|
|
873
|
+
const prepared = stripReferenceObjects(withoutObjectId(value));
|
|
874
|
+
this.db.validateWriteValue(db, coll, prepared);
|
|
875
|
+
return prepared;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
_snapshotCollections(scope) {
|
|
879
|
+
if (!scope) return this.db.snapshot();
|
|
880
|
+
|
|
881
|
+
return this.db.snapshot({
|
|
882
|
+
database: scope.database,
|
|
883
|
+
collection: scope.collection,
|
|
884
|
+
includeObject: (database, collection, id) => {
|
|
885
|
+
return this.shardForObject(database, collection, id) === scope.shard;
|
|
886
|
+
},
|
|
887
|
+
}).map((collection) => ({
|
|
888
|
+
...collection,
|
|
889
|
+
shard: scope.shard,
|
|
890
|
+
objects: collection.objects.map((object) => ({ ...object, shard: object.shard || scope.shard })),
|
|
891
|
+
}));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
_opsAfter(after, limit, scope) {
|
|
895
|
+
return [...this._opsAfterIterable(after, limit, scope)];
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
*_opsAfterIterable(after, limit, scope) {
|
|
899
|
+
let count = 0;
|
|
900
|
+
for (const op of this.log) {
|
|
901
|
+
if (op.seq <= after) continue;
|
|
902
|
+
if (scope && !this._opMatchesScope(op, scope)) continue;
|
|
903
|
+
yield op;
|
|
904
|
+
count++;
|
|
905
|
+
if (limit && count >= limit) break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
_opMatchesScope(op, scope) {
|
|
910
|
+
return (
|
|
911
|
+
(scope.database === GLOBAL_DATABASE || op.db === scope.database) &&
|
|
912
|
+
(scope.collection === GLOBAL_COLLECTION || op.coll === scope.collection) &&
|
|
913
|
+
op.shard === scope.shard
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
_peerKey(peer) {
|
|
918
|
+
return peer.nodeId || peer.key;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
_syncPeersForTarget(target) {
|
|
922
|
+
return this.url ? target.peers.filter((peer) => !this._isSelfPeer(peer)) : target.peers;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
_isSelfPeer(peer) {
|
|
926
|
+
return normalizeUrl(peer.url) === this.url || peer.nodeId === this.id || peer.key === this.id;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
_scope(database, collection, shard) {
|
|
930
|
+
if (database === GLOBAL_DATABASE && collection === GLOBAL_COLLECTION && shard === DEFAULT_SHARD) return undefined;
|
|
931
|
+
return { database, collection, shard };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
_cursorKey(peerKey, scope) {
|
|
935
|
+
return scope ? `${peerKey}|${scope.database}/${scope.collection}/${scope.shard}` : peerKey;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
_bindPeer(peer, nodeId, previousKey) {
|
|
939
|
+
if (!nodeId) return;
|
|
940
|
+
if (previousKey && previousKey !== nodeId && this.lastSeen.has(previousKey)) {
|
|
941
|
+
const seen = Math.max(this.lastSeen.get(nodeId) || 0, this.lastSeen.get(previousKey) || 0);
|
|
942
|
+
this.lastSeen.set(nodeId, seen);
|
|
943
|
+
this.lastSeen.delete(previousKey);
|
|
944
|
+
}
|
|
945
|
+
peer.nodeId = nodeId;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function withoutObjectId(object) {
|
|
950
|
+
const { _id, ...rest } = object || {};
|
|
951
|
+
return rest;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function withoutReferenceOptions(options = {}) {
|
|
955
|
+
const { refDepth, ...rest } = options;
|
|
956
|
+
return rest;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function vectorShardIndexKey(shards = []) {
|
|
960
|
+
const shardList = [...new Set((shards || []).filter(Boolean).map(String))].sort();
|
|
961
|
+
return shardList.length ? `shards:${shardList.join(",")}` : "all";
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function normalizeObjectsWithIds(objects) {
|
|
965
|
+
if (!Array.isArray(objects)) throw new TypeError("objects must be an array");
|
|
966
|
+
const normalized = objects.map((object, index) => {
|
|
967
|
+
if (!object || typeof object !== "object" || Array.isArray(object)) {
|
|
968
|
+
const err = new TypeError(`item at index ${index} must be an object`);
|
|
969
|
+
err.index = index;
|
|
970
|
+
throw err;
|
|
971
|
+
}
|
|
972
|
+
if (object._id === undefined || object._id === null || String(object._id).length === 0) {
|
|
973
|
+
const err = new TypeError(`item at index ${index} must include _id`);
|
|
974
|
+
err.index = index;
|
|
975
|
+
throw err;
|
|
976
|
+
}
|
|
977
|
+
return { ...object, _id: String(object._id) };
|
|
978
|
+
});
|
|
979
|
+
assertUniqueIds(normalized.map((object) => object._id));
|
|
980
|
+
return normalized;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function normalizeInsertObjects(objects) {
|
|
984
|
+
const normalized = (objects || []).map((obj = {}, index) => {
|
|
985
|
+
const hasId = obj && obj._id !== undefined && obj._id !== null;
|
|
986
|
+
const id = hasId ? String(obj._id) : createObjectId();
|
|
987
|
+
if (id.length === 0) {
|
|
988
|
+
const err = new TypeError(`item at index ${index} must include a non-empty _id when _id is provided`);
|
|
989
|
+
err.index = index;
|
|
990
|
+
throw err;
|
|
991
|
+
}
|
|
992
|
+
return { id, value: withoutObjectId(obj) };
|
|
993
|
+
});
|
|
994
|
+
assertUniqueIds(normalized.map((item) => item.id));
|
|
995
|
+
return normalized;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function normalizeIds(ids) {
|
|
999
|
+
if (!Array.isArray(ids)) throw new TypeError("ids must be an array");
|
|
1000
|
+
const normalized = ids.map((rawId, index) => {
|
|
1001
|
+
const id = String(rawId);
|
|
1002
|
+
if (id.length === 0) {
|
|
1003
|
+
const err = new TypeError(`id at index ${index} must be non-empty`);
|
|
1004
|
+
err.index = index;
|
|
1005
|
+
throw err;
|
|
1006
|
+
}
|
|
1007
|
+
return id;
|
|
1008
|
+
});
|
|
1009
|
+
assertUniqueIds(normalized);
|
|
1010
|
+
return normalized;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function assertUniqueIds(ids) {
|
|
1014
|
+
const seen = new Set();
|
|
1015
|
+
for (let i = 0; i < ids.length; i++) {
|
|
1016
|
+
const id = String(ids[i]);
|
|
1017
|
+
if (seen.has(id)) {
|
|
1018
|
+
const err = new TypeError(`duplicate _id in bulk request: ${id}`);
|
|
1019
|
+
err.index = i;
|
|
1020
|
+
err.id = id;
|
|
1021
|
+
throw err;
|
|
1022
|
+
}
|
|
1023
|
+
seen.add(id);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function assertInsertTargetsMissing(collection, items) {
|
|
1028
|
+
for (const item of items || []) {
|
|
1029
|
+
if (!collection.has(item.id)) continue;
|
|
1030
|
+
const err = new Error(`object already exists: ${item.id}`);
|
|
1031
|
+
err.status = 409;
|
|
1032
|
+
err.id = item.id;
|
|
1033
|
+
throw err;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function normalizeUrl(url) {
|
|
1038
|
+
return url ? String(url).replace(/\/+$/, "") : null;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function scopeKey(scope) {
|
|
1042
|
+
return `${scope.database}\n${scope.collection}\n${scope.shard}`;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function parseScopeKey(key) {
|
|
1046
|
+
const [database, collection, shard] = key.split("\n");
|
|
1047
|
+
return { database, collection, shard };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function scopeMatches(opScope, requested) {
|
|
1051
|
+
return (
|
|
1052
|
+
(requested.database === GLOBAL_DATABASE || requested.database === opScope.database) &&
|
|
1053
|
+
(requested.collection === GLOBAL_COLLECTION || requested.collection === opScope.collection) &&
|
|
1054
|
+
requested.shard === opScope.shard
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export function localPeer(node) {
|
|
1059
|
+
return {
|
|
1060
|
+
key: node.id,
|
|
1061
|
+
nodeId: node.id,
|
|
1062
|
+
pull: async (after, limit, scope) => {
|
|
1063
|
+
if (node.needsSnapshot(after, scope)) {
|
|
1064
|
+
return { status: 410, nodeId: node.id, lastSeq: node.seq, trimmed: node.trimmed };
|
|
1065
|
+
}
|
|
1066
|
+
const ops = node._opsAfter(after, limit, scope);
|
|
1067
|
+
return {
|
|
1068
|
+
status: 200,
|
|
1069
|
+
nodeId: node.id,
|
|
1070
|
+
lastSeq: node.seq,
|
|
1071
|
+
ops,
|
|
1072
|
+
};
|
|
1073
|
+
},
|
|
1074
|
+
ack: async (id, seq, scope) => node.receiveAck(id, seq, scope),
|
|
1075
|
+
};
|
|
1076
|
+
}
|