@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.
Files changed (74) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.losslessorgclient.js +1 -1
  3. package/dist_ts/core/classes.error.js +1 -1
  4. package/dist_ts/core/classes.operation.js +1 -1
  5. package/dist_ts/nosqldb/classes.atomicdelete.js +3 -3
  6. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +2 -2
  7. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +6 -3
  8. package/dist_ts/nosqldb/classes.atomicupdate.js +4 -4
  9. package/dist_ts/nosqldb/classes.bootstraprecord.js +10 -5
  10. package/dist_ts/nosqldb/classes.bsonassertion.d.ts +1 -1
  11. package/dist_ts/nosqldb/classes.bsonsnapshot.js +1 -1
  12. package/dist_ts/nosqldb/classes.collection.js +91 -38
  13. package/dist_ts/nosqldb/classes.collectiontopology.d.ts +1 -1
  14. package/dist_ts/nosqldb/classes.collectiontopology.js +1 -1
  15. package/dist_ts/nosqldb/classes.connection.js +1 -1
  16. package/dist_ts/nosqldb/classes.cursor.d.ts +4 -1
  17. package/dist_ts/nosqldb/classes.cursor.js +14 -3
  18. package/dist_ts/nosqldb/classes.cursoroperation.d.ts +1 -1
  19. package/dist_ts/nosqldb/classes.db.d.ts +22 -0
  20. package/dist_ts/nosqldb/classes.db.js +25 -1
  21. package/dist_ts/nosqldb/classes.doc.js +35 -5
  22. package/dist_ts/nosqldb/classes.exactpersistence.d.ts +6 -0
  23. package/dist_ts/nosqldb/classes.exactpersistence.js +70 -13
  24. package/dist_ts/nosqldb/classes.lucene.adapter.js +1 -1
  25. package/dist_ts/nosqldb/classes.mongoadministration.js +1 -1
  26. package/dist_ts/nosqldb/classes.namespaceinspection.js +1 -1
  27. package/dist_ts/nosqldb/classes.operationdeadline.js +1 -1
  28. package/dist_ts/nosqldb/classes.readtracker.d.ts +87 -0
  29. package/dist_ts/nosqldb/classes.readtracker.js +876 -0
  30. package/dist_ts/nosqldb/classes.session.d.ts +32 -9
  31. package/dist_ts/nosqldb/classes.session.js +116 -26
  32. package/dist_ts/nosqldb/classes.storedinspection.js +12 -4
  33. package/dist_ts/nosqldb/classes.testdatabase.js +16 -9
  34. package/dist_ts/nosqldb/classes.writeobserver.d.ts +11 -0
  35. package/dist_ts/nosqldb/classes.writeobserver.js +1 -1
  36. package/dist_ts/objectstorage/classes.bucket.js +1 -1
  37. package/dist_ts/objectstorage/classes.connection.js +1 -1
  38. package/dist_ts/objectstorage/classes.directory.js +1 -1
  39. package/dist_ts/objectstorage/classes.file.js +1 -1
  40. package/dist_ts/objectstorage/classes.listcursor.js +1 -1
  41. package/dist_ts/objectstorage/classes.smartbucket.js +1 -1
  42. package/dist_ts/objectstorage/classes.watcher.js +1 -1
  43. package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1 -1
  44. package/dist_ts/objectstorage/internal.exactupload.capability.js +1 -1
  45. package/dist_ts/objectstorage/internal.exactupload.operations.js +1 -1
  46. package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +1 -1
  47. package/dist_ts/sqldb/classes.clickhouseconnection.js +1 -1
  48. package/dist_ts/sqldb/classes.sqlconnection.js +1 -1
  49. package/dist_ts/sqldb/classes.sqltable.js +1 -1
  50. package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +1 -1
  51. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +1 -1
  52. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +1 -1
  53. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +1 -1
  54. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +1 -1
  55. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +1 -1
  56. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +1 -1
  57. package/dist_ts/sqldb/internal.sqlpool.js +1 -1
  58. package/package.json +2 -2
  59. package/readme.md +1 -0
  60. package/ts/00_commitinfo_data.ts +1 -1
  61. package/ts/nosqldb/classes.atomicdelete.ts +16 -2
  62. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +5 -0
  63. package/ts/nosqldb/classes.atomicupdate.ts +10 -1
  64. package/ts/nosqldb/classes.bootstraprecord.ts +7 -4
  65. package/ts/nosqldb/classes.collection.ts +112 -46
  66. package/ts/nosqldb/classes.cursor.ts +14 -1
  67. package/ts/nosqldb/classes.db.ts +32 -0
  68. package/ts/nosqldb/classes.doc.ts +32 -2
  69. package/ts/nosqldb/classes.exactpersistence.ts +93 -19
  70. package/ts/nosqldb/classes.readtracker.ts +1028 -0
  71. package/ts/nosqldb/classes.session.ts +163 -30
  72. package/ts/nosqldb/classes.storedinspection.ts +15 -3
  73. package/ts/nosqldb/classes.testdatabase.ts +14 -8
  74. 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(collectionArg, optsArg?.session, 'update', null, filterArg);
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 the index already in the
1846
- * namespace, yet the remedy depends on it: adopt that name in the declaration
1847
- * or drop the index. Only the collision path pays for the extra catalog read.
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
- expectedArg: INormalizedCollectionModelSchema['indexes'][number],
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 equivalentIndex = actualIndexes.find(
1867
- (actualArg) =>
1868
- actualArg.name !== expectedArg.name &&
1869
- stableValue(Object.entries(actualArg.key || {})) ===
1870
- stableValue(expectedArg.key) &&
1871
- indexEquivalenceSignature(actualArg) ===
1872
- indexEquivalenceSignature(expectedArg.options),
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 (const index of modelSchema.indexes) {
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.createIndex(
2139
- Object.fromEntries(index.key) as plugins.mongodb.IndexSpecification,
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
- index,
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
- return this.mongoDbCollection.findOne(filterObject, {
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(this, opts?.session, 'insert', [saveableObject]);
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(this, opts?.session, 'insert', inserted);
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(this, optsArg.session, 'insert', inserted);
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
- return runCursorOperation(cursor, async (cursorArg) =>
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 = cursorArg;
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[];
@@ -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({ [optionsArg.sortField]: sortDirection, [uniqueField]: sortDirection })
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((this as any).normalizeReadFilter(filterArg), {
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
  );