@lossless.org/client 1.20.1 → 1.21.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.losslessorgclient.js +1 -1
- package/dist_ts/core/classes.error.js +1 -1
- package/dist_ts/core/classes.operation.js +1 -1
- package/dist_ts/nosqldb/classes.atomicdelete.js +3 -3
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +2 -2
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +6 -3
- package/dist_ts/nosqldb/classes.atomicupdate.js +4 -4
- package/dist_ts/nosqldb/classes.bootstraprecord.js +10 -5
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +1 -1
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +1 -1
- package/dist_ts/nosqldb/classes.collection.js +91 -38
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +1 -1
- package/dist_ts/nosqldb/classes.collectiontopology.js +1 -1
- package/dist_ts/nosqldb/classes.connection.js +1 -1
- package/dist_ts/nosqldb/classes.cursor.d.ts +4 -1
- package/dist_ts/nosqldb/classes.cursor.js +14 -3
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +1 -1
- package/dist_ts/nosqldb/classes.db.d.ts +22 -0
- package/dist_ts/nosqldb/classes.db.js +25 -1
- package/dist_ts/nosqldb/classes.doc.js +35 -5
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +70 -13
- package/dist_ts/nosqldb/classes.lucene.adapter.js +1 -1
- package/dist_ts/nosqldb/classes.mongoadministration.js +1 -1
- package/dist_ts/nosqldb/classes.namespaceinspection.js +1 -1
- package/dist_ts/nosqldb/classes.operationdeadline.js +1 -1
- package/dist_ts/nosqldb/classes.readtracker.d.ts +87 -0
- package/dist_ts/nosqldb/classes.readtracker.js +876 -0
- package/dist_ts/nosqldb/classes.session.d.ts +32 -9
- package/dist_ts/nosqldb/classes.session.js +116 -26
- package/dist_ts/nosqldb/classes.storedinspection.js +12 -4
- package/dist_ts/nosqldb/classes.testdatabase.js +16 -9
- package/dist_ts/nosqldb/classes.writeobserver.d.ts +11 -0
- package/dist_ts/nosqldb/classes.writeobserver.js +1 -1
- package/dist_ts/objectstorage/classes.bucket.js +1 -1
- package/dist_ts/objectstorage/classes.connection.js +1 -1
- package/dist_ts/objectstorage/classes.directory.js +1 -1
- package/dist_ts/objectstorage/classes.file.js +1 -1
- package/dist_ts/objectstorage/classes.listcursor.js +1 -1
- package/dist_ts/objectstorage/classes.smartbucket.js +1 -1
- package/dist_ts/objectstorage/classes.watcher.js +1 -1
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1 -1
- package/dist_ts/objectstorage/internal.exactupload.capability.js +1 -1
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1 -1
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +1 -1
- package/dist_ts/sqldb/classes.clickhouseconnection.js +1 -1
- package/dist_ts/sqldb/classes.sqlconnection.js +1 -1
- package/dist_ts/sqldb/classes.sqltable.js +1 -1
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +1 -1
- package/dist_ts/sqldb/internal.sqlpool.js +1 -1
- package/package.json +2 -2
- package/readme.md +1 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.atomicdelete.ts +16 -2
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +5 -0
- package/ts/nosqldb/classes.atomicupdate.ts +10 -1
- package/ts/nosqldb/classes.bootstraprecord.ts +7 -4
- package/ts/nosqldb/classes.collection.ts +112 -46
- package/ts/nosqldb/classes.cursor.ts +14 -1
- package/ts/nosqldb/classes.db.ts +32 -0
- package/ts/nosqldb/classes.doc.ts +32 -2
- package/ts/nosqldb/classes.exactpersistence.ts +93 -19
- package/ts/nosqldb/classes.readtracker.ts +1028 -0
- package/ts/nosqldb/classes.session.ts +163 -30
- package/ts/nosqldb/classes.storedinspection.ts +15 -3
- package/ts/nosqldb/classes.testdatabase.ts +14 -8
- package/ts/nosqldb/classes.writeobserver.ts +11 -0
|
@@ -57,6 +57,7 @@ export const executeAtomicUpdate = async <T>(
|
|
|
57
57
|
result.upsertedCount > 0 ? 'insert' : 'update',
|
|
58
58
|
optsArg?.observedDocument ? [optsArg.observedDocument] : null,
|
|
59
59
|
filterArg,
|
|
60
|
+
{ images: 'none', update: updateArg },
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
return result;
|
|
@@ -122,6 +123,7 @@ export const executeAtomicUpsertMany = async <T>(
|
|
|
122
123
|
Object.hasOwn(upsertedIdsArg ?? {}, indexArg) ? 'insert' : 'update',
|
|
123
124
|
null,
|
|
124
125
|
operationArg.filter,
|
|
126
|
+
{ images: 'none', update: operationArg.update },
|
|
125
127
|
);
|
|
126
128
|
});
|
|
127
129
|
};
|
|
@@ -206,7 +208,14 @@ export const executeAtomicUpdateMany = async <T>(
|
|
|
206
208
|
},
|
|
207
209
|
);
|
|
208
210
|
if (result.matchedCount > 0) {
|
|
209
|
-
reportSmartdataCollectionWrite(
|
|
211
|
+
reportSmartdataCollectionWrite(
|
|
212
|
+
collectionArg,
|
|
213
|
+
optsArg?.session,
|
|
214
|
+
'update',
|
|
215
|
+
null,
|
|
216
|
+
filterArg,
|
|
217
|
+
{ images: 'none', update: updateArg },
|
|
218
|
+
);
|
|
210
219
|
}
|
|
211
220
|
return result;
|
|
212
221
|
} catch (errorArg) {
|
|
@@ -6,7 +6,8 @@ import { snapshotBoundedBsonDocument } from './classes.bsonsnapshot.js';
|
|
|
6
6
|
import { validateOrdinaryStoredDocument } from './classes.ordinarypersistence.js';
|
|
7
7
|
import { SmartdataPersistenceError, isMongoDuplicateKeyError } from './classes.persistence.js';
|
|
8
8
|
import { readBoundedInspectionCursor } from './classes.namespaceinspection.js';
|
|
9
|
-
import { reportSmartdataWrite } from './classes.session.js';
|
|
9
|
+
import { reportSmartdataWrite, reportSmartdataWriteFailure } from './classes.session.js';
|
|
10
|
+
import { runSmartdataWrite } from './classes.readtracker.js';
|
|
10
11
|
import {
|
|
11
12
|
collectionModelSchemaResolverSymbol, collectionTopologyDbInspectionSymbol,
|
|
12
13
|
getExpectedCollectionTopologyForSchema, inspectCollectionTopologySnapshot,
|
|
@@ -63,7 +64,7 @@ export const insertBootstrapRecord = async (
|
|
|
63
64
|
let outcome: TBootstrapOutcome;
|
|
64
65
|
try {
|
|
65
66
|
// Explicit preparation uses this same connection-generation guard.
|
|
66
|
-
outcome = await dbArg[collectionTopologyDbInspectionSymbol](async (mongoDbArg, assertCurrentArg) => {
|
|
67
|
+
outcome = await runSmartdataWrite(dbArg, () => dbArg[collectionTopologyDbInspectionSymbol](async (mongoDbArg, assertCurrentArg) => {
|
|
67
68
|
budget.check();
|
|
68
69
|
const namespaces = await readBoundedInspectionCursor(mongoDbArg.listCollections(
|
|
69
70
|
{ name: schema.collectionName },
|
|
@@ -91,18 +92,20 @@ export const insertBootstrapRecord = async (
|
|
|
91
92
|
} catch (errorArg) {
|
|
92
93
|
return isMongoDuplicateKeyError(errorArg) ? 'unique_conflict' : 'ambiguous_write';
|
|
93
94
|
}
|
|
94
|
-
});
|
|
95
|
+
}));
|
|
95
96
|
} catch {
|
|
97
|
+
if (dispatched) reportSmartdataWriteFailure(dbArg, undefined, schema.collectionName);
|
|
96
98
|
throw new SmartdataPersistenceError(dispatched ? 'ambiguous_write' : 'unsupported_operation',
|
|
97
99
|
dispatched ? 'Bootstrap insertion outcome is uncertain; inspect its explicit ID before proceeding.'
|
|
98
100
|
: 'Bootstrap insertion could not inspect the current database connection.');
|
|
99
101
|
}
|
|
100
102
|
if (outcome === 'inserted') {
|
|
101
103
|
reportSmartdataWrite(dbArg, undefined, {
|
|
102
|
-
collection: schema.collectionName, kind: 'insert', documents: [document],
|
|
104
|
+
collection: schema.collectionName, kind: 'insert', documents: [document], images: 'after',
|
|
103
105
|
});
|
|
104
106
|
return Object.freeze({ acknowledged: true, insertedId: document._id as string });
|
|
105
107
|
}
|
|
108
|
+
if (outcome === 'ambiguous_write') reportSmartdataWriteFailure(dbArg, undefined, schema.collectionName);
|
|
106
109
|
const messages = {
|
|
107
110
|
unique_conflict: 'Bootstrap record conflicts with a unique index.',
|
|
108
111
|
invalid_configuration: 'Bootstrap insertion requires absent or exact primary-only collection topology.',
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
leaseOrdinarySmartdataSession,
|
|
36
36
|
reportSmartdataCollectionWrite,
|
|
37
37
|
runWithOrdinarySmartdataSession,
|
|
38
|
+
trackSmartdataRead,
|
|
38
39
|
type TSmartdataOrdinarySession,
|
|
39
40
|
} from './classes.session.js';
|
|
40
41
|
import {
|
|
@@ -1842,14 +1843,16 @@ const indexEquivalenceSignature = (indexArg: {
|
|
|
1842
1843
|
});
|
|
1843
1844
|
|
|
1844
1845
|
/**
|
|
1845
|
-
* MongoDB reports an index collision without naming
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1846
|
+
* MongoDB reports an index collision without naming either index, yet the
|
|
1847
|
+
* remedy depends on them: adopt the existing name in the declaration or drop
|
|
1848
|
+
* the index. The declarations are checked in order against the catalog and
|
|
1849
|
+
* against one another, since one command creates them all. Only the collision
|
|
1850
|
+
* path pays for the extra catalog read.
|
|
1848
1851
|
*/
|
|
1849
1852
|
const describeIndexCollision = async (
|
|
1850
1853
|
targetCollectionArg: plugins.mongodb.Collection,
|
|
1851
1854
|
collectionNameArg: string,
|
|
1852
|
-
|
|
1855
|
+
expectedIndexesArg: INormalizedCollectionModelSchema['indexes'],
|
|
1853
1856
|
errorArg: unknown,
|
|
1854
1857
|
): Promise<unknown> => {
|
|
1855
1858
|
if (!isMongoIndexConflictError(errorArg)) {
|
|
@@ -1863,31 +1866,49 @@ const describeIndexCollision = async (
|
|
|
1863
1866
|
// own error stays the best evidence the caller can act on.
|
|
1864
1867
|
return errorArg;
|
|
1865
1868
|
}
|
|
1866
|
-
const
|
|
1867
|
-
(
|
|
1868
|
-
actualArg
|
|
1869
|
-
|
|
1870
|
-
stableValue(
|
|
1871
|
-
|
|
1872
|
-
indexEquivalenceSignature(
|
|
1873
|
-
|
|
1874
|
-
if (equivalentIndex) {
|
|
1875
|
-
return new SmartdataPersistenceError(
|
|
1876
|
-
'invalid_configuration',
|
|
1877
|
-
`Collection "${collectionNameArg}" already carries the equivalent index ` +
|
|
1878
|
-
`"${equivalentIndex.name}", so declared index "${expectedArg.name}" ` +
|
|
1879
|
-
'cannot be created. Declare the existing name — an identity declares ' +
|
|
1880
|
-
'it with @unI({ indexName }) — or drop the existing index.',
|
|
1881
|
-
{ cause: errorArg },
|
|
1882
|
-
);
|
|
1883
|
-
}
|
|
1884
|
-
if (actualIndexes.some((actualArg) => actualArg.name === expectedArg.name)) {
|
|
1885
|
-
return new SmartdataPersistenceError(
|
|
1886
|
-
'invalid_configuration',
|
|
1887
|
-
`Collection "${collectionNameArg}" already carries an index named ` +
|
|
1888
|
-
`"${expectedArg.name}" with a different key or options.`,
|
|
1889
|
-
{ cause: errorArg },
|
|
1869
|
+
for (const [position, expected] of expectedIndexesArg.entries()) {
|
|
1870
|
+
const equivalentIndex = actualIndexes.find(
|
|
1871
|
+
(actualArg) =>
|
|
1872
|
+
actualArg.name !== expected.name &&
|
|
1873
|
+
stableValue(Object.entries(actualArg.key || {})) ===
|
|
1874
|
+
stableValue(expected.key) &&
|
|
1875
|
+
indexEquivalenceSignature(actualArg) ===
|
|
1876
|
+
indexEquivalenceSignature(expected.options),
|
|
1890
1877
|
);
|
|
1878
|
+
if (equivalentIndex) {
|
|
1879
|
+
return new SmartdataPersistenceError(
|
|
1880
|
+
'invalid_configuration',
|
|
1881
|
+
`Collection "${collectionNameArg}" already carries the equivalent index ` +
|
|
1882
|
+
`"${equivalentIndex.name}", so declared index "${expected.name}" ` +
|
|
1883
|
+
'cannot be created. Declare the existing name — an identity declares ' +
|
|
1884
|
+
'it with @unI({ indexName }) — or drop the existing index.',
|
|
1885
|
+
{ cause: errorArg },
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
if (actualIndexes.some((actualArg) => actualArg.name === expected.name)) {
|
|
1889
|
+
return new SmartdataPersistenceError(
|
|
1890
|
+
'invalid_configuration',
|
|
1891
|
+
`Collection "${collectionNameArg}" already carries an index named ` +
|
|
1892
|
+
`"${expected.name}" with a different key or options.`,
|
|
1893
|
+
{ cause: errorArg },
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
const equivalentDeclaration = expectedIndexesArg
|
|
1897
|
+
.slice(0, position)
|
|
1898
|
+
.find(
|
|
1899
|
+
(earlierArg) =>
|
|
1900
|
+
stableValue(earlierArg.key) === stableValue(expected.key) &&
|
|
1901
|
+
indexEquivalenceSignature(earlierArg.options) ===
|
|
1902
|
+
indexEquivalenceSignature(expected.options),
|
|
1903
|
+
);
|
|
1904
|
+
if (equivalentDeclaration) {
|
|
1905
|
+
return new SmartdataPersistenceError(
|
|
1906
|
+
'invalid_configuration',
|
|
1907
|
+
`Collection "${collectionNameArg}" declares the equivalent indexes ` +
|
|
1908
|
+
`"${equivalentDeclaration.name}" and "${expected.name}"; declare one name.`,
|
|
1909
|
+
{ cause: errorArg },
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1891
1912
|
}
|
|
1892
1913
|
// An index over the same key that is not interchangeable with the
|
|
1893
1914
|
// declaration cannot be adopted by name, and MongoDB's own error already
|
|
@@ -2133,20 +2154,22 @@ export class SmartdataCollection<T> {
|
|
|
2133
2154
|
if (!modelSchema) {
|
|
2134
2155
|
return;
|
|
2135
2156
|
}
|
|
2136
|
-
for
|
|
2157
|
+
// One command for every declared index: the server builds the missing
|
|
2158
|
+
// ones together, or none of them when one collides.
|
|
2159
|
+
if (modelSchema.indexes.length > 0) {
|
|
2137
2160
|
try {
|
|
2138
|
-
await targetCollection.
|
|
2139
|
-
|
|
2140
|
-
{
|
|
2161
|
+
await targetCollection.createIndexes(
|
|
2162
|
+
modelSchema.indexes.map((index) => ({
|
|
2141
2163
|
...index.options,
|
|
2164
|
+
key: Object.fromEntries(index.key) as plugins.mongodb.IndexDescription['key'],
|
|
2142
2165
|
name: index.name,
|
|
2143
|
-
},
|
|
2166
|
+
})),
|
|
2144
2167
|
);
|
|
2145
2168
|
} catch (errorArg) {
|
|
2146
2169
|
throw await describeIndexCollision(
|
|
2147
2170
|
targetCollection,
|
|
2148
2171
|
this.collectionName,
|
|
2149
|
-
|
|
2172
|
+
modelSchema.indexes,
|
|
2150
2173
|
errorArg,
|
|
2151
2174
|
);
|
|
2152
2175
|
}
|
|
@@ -2400,12 +2423,20 @@ export class SmartdataCollection<T> {
|
|
|
2400
2423
|
{ ordinaryWrite: false, timeoutMS: opts?.timeoutMS },
|
|
2401
2424
|
async (rawSessionArg, deadlineArg) => {
|
|
2402
2425
|
await deadlineArg.join(this.init());
|
|
2403
|
-
|
|
2426
|
+
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
2427
|
+
kind: 'documents',
|
|
2428
|
+
filter: filterObject,
|
|
2429
|
+
limited: true,
|
|
2430
|
+
});
|
|
2431
|
+
const document = await this.mongoDbCollection.findOne(filterObject, {
|
|
2404
2432
|
projection: opts?.projection,
|
|
2405
2433
|
maxTimeMS: opts?.maxTimeMS,
|
|
2406
2434
|
session: rawSessionArg,
|
|
2407
2435
|
...deadlineArg.commandOptions(),
|
|
2408
2436
|
});
|
|
2437
|
+
if (document) tracked?.record(document);
|
|
2438
|
+
tracked?.settle();
|
|
2439
|
+
return document;
|
|
2409
2440
|
},
|
|
2410
2441
|
);
|
|
2411
2442
|
}
|
|
@@ -2426,10 +2457,14 @@ export class SmartdataCollection<T> {
|
|
|
2426
2457
|
});
|
|
2427
2458
|
try {
|
|
2428
2459
|
await this.init();
|
|
2460
|
+
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
2461
|
+
kind: 'documents',
|
|
2462
|
+
filter: filterObjectArg,
|
|
2463
|
+
});
|
|
2429
2464
|
const cursor = this.mongoDbCollection.find(filterObjectArg, {
|
|
2430
2465
|
session: lease.rawSession,
|
|
2431
2466
|
});
|
|
2432
|
-
return new SmartdataDbCursor(cursor, dbDocArg, lease.release);
|
|
2467
|
+
return new SmartdataDbCursor(cursor, dbDocArg, lease.release, tracked);
|
|
2433
2468
|
} catch (cursorCreationError) {
|
|
2434
2469
|
lease.release();
|
|
2435
2470
|
throw cursorCreationError;
|
|
@@ -2448,6 +2483,10 @@ export class SmartdataCollection<T> {
|
|
|
2448
2483
|
{ ordinaryWrite: false, timeoutMS: opts?.timeoutMS },
|
|
2449
2484
|
async (rawSessionArg, deadlineArg) => {
|
|
2450
2485
|
await deadlineArg.join(this.init());
|
|
2486
|
+
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
2487
|
+
kind: 'documents',
|
|
2488
|
+
filter: filterObject,
|
|
2489
|
+
});
|
|
2451
2490
|
const cursor = this.mongoDbCollection.find(filterObject, {
|
|
2452
2491
|
session: rawSessionArg,
|
|
2453
2492
|
...deadlineArg.cursorOptions(),
|
|
@@ -2460,6 +2499,10 @@ export class SmartdataCollection<T> {
|
|
|
2460
2499
|
(cursorArg) => cursorArg.toArray(),
|
|
2461
2500
|
deadlineArg.signal,
|
|
2462
2501
|
);
|
|
2502
|
+
if (tracked) {
|
|
2503
|
+
for (const document of result) tracked.record(document);
|
|
2504
|
+
tracked.settle();
|
|
2505
|
+
}
|
|
2463
2506
|
|
|
2464
2507
|
// In-memory check for duplicate _id values (should never happen)
|
|
2465
2508
|
if (result.length > 0) {
|
|
@@ -2603,7 +2646,9 @@ export class SmartdataCollection<T> {
|
|
|
2603
2646
|
...deadlineArg.commandOptions(),
|
|
2604
2647
|
});
|
|
2605
2648
|
if (result.acknowledged) {
|
|
2606
|
-
reportSmartdataCollectionWrite(
|
|
2649
|
+
reportSmartdataCollectionWrite(
|
|
2650
|
+
this, opts?.session, 'insert', [saveableObject], undefined, { images: 'after' },
|
|
2651
|
+
);
|
|
2607
2652
|
}
|
|
2608
2653
|
return result;
|
|
2609
2654
|
} catch (err: any) {
|
|
@@ -2646,6 +2691,12 @@ export class SmartdataCollection<T> {
|
|
|
2646
2691
|
{ ordinaryWrite: false, timeoutMS: opts?.timeoutMS, signal: opts?.signal },
|
|
2647
2692
|
async (rawSessionArg, deadlineArg) => {
|
|
2648
2693
|
await deadlineArg.join(this.init());
|
|
2694
|
+
// Totals depend on every matching document, not on which ones come back.
|
|
2695
|
+
trackSmartdataRead(this, opts?.session, {
|
|
2696
|
+
kind: 'documents',
|
|
2697
|
+
filter: matchArg ?? {},
|
|
2698
|
+
matches: true,
|
|
2699
|
+
});
|
|
2649
2700
|
const pipeline: plugins.mongodb.Document[] = [];
|
|
2650
2701
|
if (matchArg && Object.keys(matchArg).length > 0) {
|
|
2651
2702
|
pipeline.push({ $match: matchArg });
|
|
@@ -2719,7 +2770,9 @@ export class SmartdataCollection<T> {
|
|
|
2719
2770
|
.map((indexArg) => saveableObjects[Number(indexArg)])
|
|
2720
2771
|
.filter((documentArg) => documentArg !== undefined);
|
|
2721
2772
|
if (inserted.length > 0) {
|
|
2722
|
-
reportSmartdataCollectionWrite(
|
|
2773
|
+
reportSmartdataCollectionWrite(
|
|
2774
|
+
this, opts?.session, 'insert', inserted, undefined, { images: 'after' },
|
|
2775
|
+
);
|
|
2723
2776
|
}
|
|
2724
2777
|
};
|
|
2725
2778
|
try {
|
|
@@ -2784,7 +2837,9 @@ export class SmartdataCollection<T> {
|
|
|
2784
2837
|
.map((indexArg) => prepared[Number(indexArg)])
|
|
2785
2838
|
.filter((documentArg) => documentArg !== undefined);
|
|
2786
2839
|
if (inserted.length > 0) {
|
|
2787
|
-
reportSmartdataCollectionWrite(
|
|
2840
|
+
reportSmartdataCollectionWrite(
|
|
2841
|
+
this, optsArg.session, 'insert', inserted, undefined, { images: 'after' },
|
|
2842
|
+
);
|
|
2788
2843
|
}
|
|
2789
2844
|
};
|
|
2790
2845
|
try {
|
|
@@ -2859,15 +2914,16 @@ export class SmartdataCollection<T> {
|
|
|
2859
2914
|
}
|
|
2860
2915
|
updateableObject[key] = saveableObject[key];
|
|
2861
2916
|
}
|
|
2917
|
+
const update = {
|
|
2918
|
+
$set: updateableObject,
|
|
2919
|
+
...(Object.keys(insertOnlyObject).length > 0
|
|
2920
|
+
? { $setOnInsert: insertOnlyObject }
|
|
2921
|
+
: {}),
|
|
2922
|
+
};
|
|
2862
2923
|
try {
|
|
2863
2924
|
const result = await this.mongoDbCollection.updateOne(
|
|
2864
2925
|
identifiableObject,
|
|
2865
|
-
|
|
2866
|
-
$set: updateableObject,
|
|
2867
|
-
...(Object.keys(insertOnlyObject).length > 0
|
|
2868
|
-
? { $setOnInsert: insertOnlyObject }
|
|
2869
|
-
: {}),
|
|
2870
|
-
},
|
|
2926
|
+
update,
|
|
2871
2927
|
{ upsert: true, session: rawSessionArg, ...deadlineArg.commandOptions() },
|
|
2872
2928
|
);
|
|
2873
2929
|
if (result.upsertedCount > 0 || result.matchedCount > 0) {
|
|
@@ -2877,6 +2933,7 @@ export class SmartdataCollection<T> {
|
|
|
2877
2933
|
result.upsertedCount > 0 ? 'insert' : 'update',
|
|
2878
2934
|
[saveableObject],
|
|
2879
2935
|
identifiableObject,
|
|
2936
|
+
{ images: 'none', update },
|
|
2880
2937
|
);
|
|
2881
2938
|
}
|
|
2882
2939
|
return result;
|
|
@@ -2917,6 +2974,7 @@ export class SmartdataCollection<T> {
|
|
|
2917
2974
|
'delete',
|
|
2918
2975
|
observedDocument ? [observedDocument] : null,
|
|
2919
2976
|
identifiableObject,
|
|
2977
|
+
{ images: 'none' },
|
|
2920
2978
|
);
|
|
2921
2979
|
}
|
|
2922
2980
|
return result;
|
|
@@ -2968,6 +3026,11 @@ export class SmartdataCollection<T> {
|
|
|
2968
3026
|
{ ordinaryWrite: false, timeoutMS: opts?.timeoutMS, signal: opts?.signal },
|
|
2969
3027
|
async (rawSessionArg, deadlineArg) => {
|
|
2970
3028
|
await deadlineArg.join(this.init());
|
|
3029
|
+
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
3030
|
+
kind: 'count',
|
|
3031
|
+
filter: filterObject,
|
|
3032
|
+
hint: opts?.hint,
|
|
3033
|
+
});
|
|
2971
3034
|
const pipeline: plugins.mongodb.Document[] = [{ $match: filterObject }];
|
|
2972
3035
|
if (opts?.limit !== undefined) {
|
|
2973
3036
|
pipeline.push({ $limit: opts.limit });
|
|
@@ -2979,9 +3042,11 @@ export class SmartdataCollection<T> {
|
|
|
2979
3042
|
...deadlineArg.commandOptions(),
|
|
2980
3043
|
session: rawSessionArg,
|
|
2981
3044
|
});
|
|
2982
|
-
|
|
3045
|
+
const count = await runCursorOperation(cursor, async (cursorArg) =>
|
|
2983
3046
|
(await cursorArg.next())?.count ?? 0,
|
|
2984
3047
|
deadlineArg.signal);
|
|
3048
|
+
tracked?.settle();
|
|
3049
|
+
return count;
|
|
2985
3050
|
},
|
|
2986
3051
|
);
|
|
2987
3052
|
}
|
|
@@ -3043,6 +3108,7 @@ export class SmartdataCollection<T> {
|
|
|
3043
3108
|
duplicateFields: Array<{ field: string; duplicateValues: number }>;
|
|
3044
3109
|
}> {
|
|
3045
3110
|
await this.init();
|
|
3111
|
+
trackSmartdataRead(this, undefined, { kind: 'collection' });
|
|
3046
3112
|
const result = {
|
|
3047
3113
|
ok: true,
|
|
3048
3114
|
estimatedCount: 0,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SmartDataDbDoc } from './classes.doc.js';
|
|
2
|
+
import type { ISmartdataReadRecorder } from './classes.readtracker.js';
|
|
2
3
|
import * as plugins from './plugins.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -16,14 +17,23 @@ export class SmartdataDbCursor<T = any> {
|
|
|
16
17
|
* raw driver session or no session at all.
|
|
17
18
|
*/
|
|
18
19
|
private releaseSession: () => void;
|
|
20
|
+
/** Records the read for open read sets; it settles once the cursor is exhausted. */
|
|
21
|
+
private readRecorder?: ISmartdataReadRecorder;
|
|
19
22
|
constructor(
|
|
20
23
|
cursorArg: plugins.mongodb.FindCursor<T>,
|
|
21
24
|
dbDocArg: typeof SmartDataDbDoc,
|
|
22
25
|
releaseSessionArg: () => void = () => {},
|
|
26
|
+
readRecorderArg?: ISmartdataReadRecorder,
|
|
23
27
|
) {
|
|
24
|
-
this.mongodbCursor =
|
|
28
|
+
this.mongodbCursor = readRecorderArg
|
|
29
|
+
? cursorArg.map((documentArg) => {
|
|
30
|
+
readRecorderArg.record(documentArg);
|
|
31
|
+
return documentArg;
|
|
32
|
+
})
|
|
33
|
+
: cursorArg;
|
|
25
34
|
this.smartdataDbDoc = dbDocArg;
|
|
26
35
|
this.releaseSession = releaseSessionArg;
|
|
36
|
+
this.readRecorder = readRecorderArg;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
public async next(closeAtEnd = true): Promise<T | null> {
|
|
@@ -34,6 +44,7 @@ export class SmartdataDbCursor<T = any> {
|
|
|
34
44
|
return await this.closeAfterError(readError);
|
|
35
45
|
}
|
|
36
46
|
if (nextDocument === null) {
|
|
47
|
+
this.readRecorder?.settle();
|
|
37
48
|
if (closeAtEnd) {
|
|
38
49
|
await this.close();
|
|
39
50
|
}
|
|
@@ -59,6 +70,7 @@ export class SmartdataDbCursor<T = any> {
|
|
|
59
70
|
await forEachFuncArg(nextClassInstance as any);
|
|
60
71
|
}
|
|
61
72
|
} while (nextDocument);
|
|
73
|
+
this.readRecorder?.settle();
|
|
62
74
|
} catch (iterationError) {
|
|
63
75
|
if (closeCursorAtEnd) {
|
|
64
76
|
await this.closeAfterError(iterationError);
|
|
@@ -74,6 +86,7 @@ export class SmartdataDbCursor<T = any> {
|
|
|
74
86
|
let convertedResult: T[];
|
|
75
87
|
try {
|
|
76
88
|
const result = await this.mongodbCursor.toArray();
|
|
89
|
+
this.readRecorder?.settle();
|
|
77
90
|
convertedResult = result.map((itemArg) =>
|
|
78
91
|
this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(itemArg),
|
|
79
92
|
) as T[];
|
package/ts/nosqldb/classes.db.ts
CHANGED
|
@@ -24,8 +24,15 @@ import {
|
|
|
24
24
|
addSmartdataWriteObserver,
|
|
25
25
|
type ISmartdataWriteObserverOptions,
|
|
26
26
|
} from './classes.writeobserver.js';
|
|
27
|
+
import {
|
|
28
|
+
changeSmartdataReadSetsOfDb,
|
|
29
|
+
trackSmartdataReads,
|
|
30
|
+
type ISmartdataTrackedReads,
|
|
31
|
+
} from './classes.readtracker.js';
|
|
27
32
|
|
|
28
33
|
export { SmartdataSession } from './classes.session.js';
|
|
34
|
+
export { SmartdataReadSet } from './classes.readtracker.js';
|
|
35
|
+
export type { ISmartdataTrackedReads } from './classes.readtracker.js';
|
|
29
36
|
export type {
|
|
30
37
|
ISmartdataTransactionOptions,
|
|
31
38
|
TSmartdataOrdinarySession,
|
|
@@ -358,6 +365,7 @@ export class SmartdataDb {
|
|
|
358
365
|
this.beginLifecycleOperation('close');
|
|
359
366
|
this.transitionLifecycle('closing');
|
|
360
367
|
this.status = 'disconnected';
|
|
368
|
+
changeSmartdataReadSetsOfDb(this);
|
|
361
369
|
const mongoDbClient = this.pendingClient ?? this.mongoDbClient;
|
|
362
370
|
try {
|
|
363
371
|
const closeErrors: unknown[] = [];
|
|
@@ -544,6 +552,30 @@ export class SmartdataDb {
|
|
|
544
552
|
return addSmartdataWriteObserver(this, optionsArg);
|
|
545
553
|
}
|
|
546
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Runs `fnArg` and records every model read it makes on this database:
|
|
557
|
+
* `findOne`, `findAll`, `getCursor`, counts, pages and the exact reads.
|
|
558
|
+
* Each read is kept as its query and the ids it returned. The returned
|
|
559
|
+
* `reads.changed` turns true once a write this process makes through the
|
|
560
|
+
* database's models could have changed any of those results; a write it
|
|
561
|
+
* cannot place, such as an update by a selector it cannot evaluate, counts
|
|
562
|
+
* as one that did. A cache keyed by `reads` rebuilds only what such a write
|
|
563
|
+
* touched.
|
|
564
|
+
*
|
|
565
|
+
* Writes of other processes, raw driver access and migrations are not
|
|
566
|
+
* followed. A read inside a transaction, a read of another database and a
|
|
567
|
+
* write made inside `fnArg` mark the set changed at once. Queries are
|
|
568
|
+
* evaluated by MongoDB's simple binary comparison, so a read of a view or of
|
|
569
|
+
* a collection with a default collation marks the set changed as well; each
|
|
570
|
+
* read collection is checked once per connection when `fnArg` returns.
|
|
571
|
+
* Close a set that is no longer needed.
|
|
572
|
+
*/
|
|
573
|
+
public async trackReads<TResult>(
|
|
574
|
+
fnArg: () => TResult | Promise<TResult>,
|
|
575
|
+
): Promise<ISmartdataTrackedReads<TResult>> {
|
|
576
|
+
return trackSmartdataReads(this, fnArg);
|
|
577
|
+
}
|
|
578
|
+
|
|
547
579
|
/**
|
|
548
580
|
* Creates a narrowly scoped MongoDB administration facade bound to this
|
|
549
581
|
* SmartData lifecycle. Applications never need direct driver access.
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
isOrdinarySessionInTransaction,
|
|
44
44
|
leaseOrdinarySmartdataSession,
|
|
45
45
|
runWithOrdinarySmartdataSession,
|
|
46
|
+
trackSmartdataRead,
|
|
46
47
|
type TSmartdataOrdinarySession,
|
|
47
48
|
} from './classes.session.js';
|
|
48
49
|
import {
|
|
@@ -6055,24 +6056,39 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6055
6056
|
timeoutMS,
|
|
6056
6057
|
}, async (rawSessionArg, deadlineArg) => {
|
|
6057
6058
|
await deadlineArg.join(collection.init());
|
|
6059
|
+
const sort = { [optionsArg.sortField]: sortDirection, [uniqueField]: sortDirection } as const;
|
|
6060
|
+
const tracked = trackSmartdataRead(collection, optionsArg.session, {
|
|
6061
|
+
kind: 'documents',
|
|
6062
|
+
filter: selector,
|
|
6063
|
+
sort,
|
|
6064
|
+
limited: true,
|
|
6065
|
+
});
|
|
6058
6066
|
const rawCursor = collection.mongoDbCollection
|
|
6059
6067
|
.find(selector, { session: rawSessionArg, ...deadlineArg.cursorOptions() })
|
|
6060
|
-
.sort(
|
|
6068
|
+
.sort(sort)
|
|
6061
6069
|
.limit(limit + 1);
|
|
6062
6070
|
return runCursorOperation(rawCursor, async (cursorArg) => {
|
|
6063
6071
|
const rawDocuments = await cursorArg.toArray();
|
|
6072
|
+
if (tracked) {
|
|
6073
|
+
for (const rawDocument of rawDocuments) tracked.record(rawDocument);
|
|
6074
|
+
tracked.settle();
|
|
6075
|
+
}
|
|
6064
6076
|
const hasMore = rawDocuments.length > limit;
|
|
6065
6077
|
const pageRows = rawDocuments.slice(0, limit);
|
|
6066
6078
|
const documents = pageRows.map(
|
|
6067
6079
|
(rawDocument: any) => (this as any).createInstanceFromMongoDbNativeDoc(rawDocument) as T,
|
|
6068
6080
|
);
|
|
6069
6081
|
const lastRow: any = pageRows[pageRows.length - 1];
|
|
6082
|
+
const countTracked = optionsArg.withTotal
|
|
6083
|
+
? trackSmartdataRead(collection, optionsArg.session, { kind: 'count', filter: baseSelector })
|
|
6084
|
+
: undefined;
|
|
6070
6085
|
const total = optionsArg.withTotal
|
|
6071
6086
|
? await collection.mongoDbCollection.countDocuments(baseSelector, {
|
|
6072
6087
|
session: rawSessionArg,
|
|
6073
6088
|
...deadlineArg.commandOptions(),
|
|
6074
6089
|
})
|
|
6075
6090
|
: undefined;
|
|
6091
|
+
countTracked?.settle();
|
|
6076
6092
|
return {
|
|
6077
6093
|
documents,
|
|
6078
6094
|
...(hasMore && lastRow
|
|
@@ -6211,8 +6227,14 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6211
6227
|
});
|
|
6212
6228
|
try {
|
|
6213
6229
|
await collection.init();
|
|
6230
|
+
const filter = (this as any).normalizeReadFilter(filterArg);
|
|
6231
|
+
// A modifier changes the query in ways a read set cannot follow, and a
|
|
6232
|
+
// skipped page depends on the documents before it.
|
|
6233
|
+
const tracked = trackSmartdataRead(collection, session, modifier
|
|
6234
|
+
? { kind: 'collection' }
|
|
6235
|
+
: { kind: 'documents', filter, sort, hint, matches: skip !== undefined && skip > 0, limited: Boolean(limit) });
|
|
6214
6236
|
let rawCursor: plugins.mongodb.FindCursor<any> =
|
|
6215
|
-
collection.mongoDbCollection.find(
|
|
6237
|
+
collection.mongoDbCollection.find(filter, {
|
|
6216
6238
|
projection,
|
|
6217
6239
|
session: lease.rawSession,
|
|
6218
6240
|
hint,
|
|
@@ -6261,6 +6283,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6261
6283
|
rawCursor,
|
|
6262
6284
|
this as any as typeof SmartDataDbDoc,
|
|
6263
6285
|
lease.release,
|
|
6286
|
+
tracked,
|
|
6264
6287
|
);
|
|
6265
6288
|
} catch (cursorCreationError) {
|
|
6266
6289
|
lease.release();
|
|
@@ -6309,12 +6332,19 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6309
6332
|
defaultTimeoutMS: 5_000,
|
|
6310
6333
|
signal: opts.signal,
|
|
6311
6334
|
}, async (rawSessionArg, deadlineArg) => {
|
|
6335
|
+
const tracked = trackSmartdataRead(collection, opts.session, {
|
|
6336
|
+
kind: 'documents', filter, sort, hint, matches: skip > 0, limited: true,
|
|
6337
|
+
});
|
|
6312
6338
|
const rawCursor = collection.mongoDbCollection.find(filter, {
|
|
6313
6339
|
projection, sort, hint, skip, limit, maxTimeMS,
|
|
6314
6340
|
...deadlineArg.cursorOptions(), session: rawSessionArg,
|
|
6315
6341
|
});
|
|
6316
6342
|
return runCursorOperation(rawCursor, async (cursorArg) => {
|
|
6317
6343
|
const documents = await cursorArg.toArray();
|
|
6344
|
+
if (tracked) {
|
|
6345
|
+
for (const document of documents) tracked.record(document);
|
|
6346
|
+
tracked.settle();
|
|
6347
|
+
}
|
|
6318
6348
|
return documents.map((documentArg) =>
|
|
6319
6349
|
(this as any).createInstanceFromMongoDbNativeDoc(documentArg) as T,
|
|
6320
6350
|
);
|