@lossless.org/client 1.23.1 → 1.23.3

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.
@@ -15,6 +15,8 @@ import {
15
15
  type TSmartdataOrdinarySession,
16
16
  } from './classes.session.js';
17
17
  import { validateOrdinaryStoredDocument } from './classes.ordinarypersistence.js';
18
+ import { updateLeavesField } from './classes.readtracker.js';
19
+ import { writeRevisionsPartitionField } from './classes.writerevisions.js';
18
20
 
19
21
  type TRawDocument = plugins.mongodb.WithId<plugins.mongodb.Document>;
20
22
 
@@ -129,11 +131,14 @@ export const executeAtomicFindOneAndUpdate = async <T>(
129
131
  }
130
132
  await deadlineArg.join(collectionArg.init());
131
133
  try {
132
- // A before image states the document's previous partition itself.
133
- const previousPartitionsRead = optionsArg.returnDocument === 'after'
134
- && await readPreviousWriteRevisions(
135
- collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
136
- );
134
+ // A before image states the document's previous partition itself, and
135
+ // so does an after image of an update that leaves the partition field
136
+ // alone: the field holds afterwards what it held before.
137
+ const partitionField = writeRevisionsPartitionField(collectionArg.smartdataDb);
138
+ const imageStatesPreviousPartition = optionsArg.returnDocument === 'before'
139
+ || (partitionField !== undefined && updateLeavesField(updateArg, partitionField));
140
+ const previousPartitionsRead = !imageStatesPreviousPartition
141
+ && await readPreviousWriteRevisions(collectionArg, rawSessionArg, filterArg, deadlineArg);
137
142
  const result = await collectionArg.mongoDbCollection.findOneAndUpdate(
138
143
  filterArg,
139
144
  updateArg,
@@ -226,7 +231,7 @@ export const executeAtomicFindOneAndReplace = async <T>(
226
231
  try {
227
232
  const previousPartitionsRead = optionsArg.returnDocument === 'after'
228
233
  && await readPreviousWriteRevisions(
229
- collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
234
+ collectionArg, rawSessionArg, filterArg, deadlineArg,
230
235
  );
231
236
  const result = await collectionArg.mongoDbCollection.findOneAndReplace(
232
237
  filterArg,
@@ -44,7 +44,7 @@ export const executeAtomicUpdate = async <T>(
44
44
  await deadlineArg.join(collectionArg.init());
45
45
  try {
46
46
  const previousPartitionsRead = await readPreviousWriteRevisions(
47
- collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
47
+ collectionArg, rawSessionArg, filterArg, deadlineArg,
48
48
  );
49
49
  const result = await collectionArg.mongoDbCollection.updateOne(
50
50
  filterArg,
@@ -140,8 +140,7 @@ export const executeAtomicUpsertMany = async <T>(
140
140
  };
141
141
  try {
142
142
  previousPartitionsRead = await readPreviousWriteRevisions(
143
- collectionArg.smartdataDb,
144
- collectionArg.mongoDbCollection,
143
+ collectionArg,
145
144
  rawSessionArg,
146
145
  { $or: operationsArg.map((operationArg) => operationArg.filter) },
147
146
  deadlineArg,
@@ -219,7 +218,7 @@ export const executeAtomicUpdateMany = async <T>(
219
218
  await deadlineArg.join(collectionArg.init());
220
219
  try {
221
220
  const previousPartitionsRead = await readPreviousWriteRevisions(
222
- collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
221
+ collectionArg, rawSessionArg, filterArg, deadlineArg,
223
222
  );
224
223
  const result = await collectionArg.mongoDbCollection.updateMany(
225
224
  filterArg,
@@ -2924,7 +2924,7 @@ export class SmartdataCollection<T> {
2924
2924
  };
2925
2925
  try {
2926
2926
  const previousPartitionsRead = await readPreviousWriteRevisions(
2927
- this.smartdataDb, this.mongoDbCollection, rawSessionArg, identifiableObject, deadlineArg,
2927
+ this, rawSessionArg, identifiableObject, deadlineArg,
2928
2928
  );
2929
2929
  const result = await this.mongoDbCollection.updateOne(
2930
2930
  identifiableObject,
@@ -2969,7 +2969,7 @@ export class SmartdataCollection<T> {
2969
2969
  ? await dbDocArg.createSavableObject() as object
2970
2970
  : undefined;
2971
2971
  const previousPartitionsRead = await readPreviousWriteRevisions(
2972
- this.smartdataDb, this.mongoDbCollection, rawSessionArg, identifiableObject, deadlineArg,
2972
+ this, rawSessionArg, identifiableObject, deadlineArg,
2973
2973
  );
2974
2974
  const result = await this.mongoDbCollection.deleteOne(identifiableObject, {
2975
2975
  session: rawSessionArg,
@@ -1956,7 +1956,7 @@ export class SmartdataExactCollection<
1956
1956
  // the write-revision partition it held is read first.
1957
1957
  const previousPartitionsRead = current._smartdataRevision === undefined
1958
1958
  && await readPreviousWriteRevisions(
1959
- this.collection.smartdataDb, this.collection.mongoDbCollection, sessionArg, selector, deadlineArg,
1959
+ this.collection, sessionArg, selector, deadlineArg,
1960
1960
  );
1961
1961
  const reportTransition = (): void => {
1962
1962
  reportSmartdataCollectionWrite(
@@ -2064,7 +2064,7 @@ export class SmartdataExactCollection<
2064
2064
  };
2065
2065
  const previousPartitionsRead = current._smartdataRevision === undefined
2066
2066
  && await readPreviousWriteRevisions(
2067
- this.collection.smartdataDb, this.collection.mongoDbCollection, sessionArg, selector, deadlineArg,
2067
+ this.collection, sessionArg, selector, deadlineArg,
2068
2068
  );
2069
2069
  let result: plugins.mongodb.DeleteResult;
2070
2070
  const deleteOptions = { session: sessionArg, ...deadlineArg.commandOptions() };
@@ -1162,6 +1162,16 @@ export const captureSmartdataCollectionChange = (
1162
1162
  return () => applyChanges(dbArg, collectionArg, [{ kind: 'unknown', touched: 'all', startedAt: 0 }]);
1163
1163
  };
1164
1164
 
1165
+ /**
1166
+ * @internal Whether an update by operators leaves a field as each document it
1167
+ * updates held it: no operator names the field or a path through it. An update
1168
+ * the tracker cannot follow may change any field.
1169
+ */
1170
+ export const updateLeavesField = (updateArg: unknown, fieldArg: string): boolean => {
1171
+ const { touched } = analyzeUpdate(updateArg, false);
1172
+ return touched !== 'all' && ![...touched].some((pathArg) => pathsOverlap(pathArg, fieldArg));
1173
+ };
1174
+
1165
1175
  /**
1166
1176
  * @internal The write-revision counters one acknowledged write bumps: the
1167
1177
  * partition of each document state it names before and after, and the shared
@@ -1067,19 +1067,26 @@ const bumpTransactionRevisions = (
1067
1067
  * documents a write by selector is about to change, and returns whether it
1068
1068
  * did; the write's report then states `previousPartitionsRead`. `false` when
1069
1069
  * the database keeps no revisions or the write runs outside an owned
1070
- * transaction.
1070
+ * transaction. `false` without a read, too, when the collection's model
1071
+ * persists no partition field: its documents count on the shared counter,
1072
+ * which the write's report bumps for each document state it cannot place, and
1073
+ * every read of the collection depends on that counter.
1071
1074
  */
1072
1075
  export const readPreviousWriteRevisions = async (
1073
- ownerArg: SmartdataDb,
1074
- collectionArg: plugins.mongodb.Collection<any>,
1076
+ collectionArg: SmartdataCollection<any>,
1075
1077
  rawSessionArg: plugins.mongodb.ClientSession | undefined,
1076
1078
  selectorArg: unknown,
1077
1079
  deadlineArg: SmartdataOperationDeadline,
1078
1080
  ): Promise<boolean> => {
1079
- if (!rawSessionArg || !isSmartdataWriteRevisionsActive(ownerArg)) return false;
1081
+ const owner = collectionArg.smartdataDb;
1082
+ if (!rawSessionArg || !isSmartdataWriteRevisionsActive(owner)) return false;
1080
1083
  const binding = bindingsByRawSession.get(rawSessionArg);
1081
- if (!binding?.transactionActive || binding.owner !== ownerArg) return false;
1082
- const ids = await readPreviousRevisionIds(ownerArg, collectionArg, rawSessionArg, selectorArg, deadlineArg);
1084
+ if (!binding?.transactionActive || binding.owner !== owner) return false;
1085
+ const persistedFields = collectionArg.getBoundModelSchema()?.persistedFields;
1086
+ if (persistedFields && !persistedFields.includes(writeRevisionsPartitionField(owner)!)) return false;
1087
+ const ids = await readPreviousRevisionIds(
1088
+ owner, collectionArg.mongoDbCollection, rawSessionArg, selectorArg, deadlineArg,
1089
+ );
1083
1090
  bumpTransactionRevisions(binding, ids);
1084
1091
  return true;
1085
1092
  };
@@ -243,6 +243,14 @@ export class Directory {
243
243
 
244
244
 
245
245
  const getDirectory = async (directoryArg: Directory, dirNameToSearch: string, isFinalDirectory: boolean) => {
246
+ // A listed directory and a new one of the same name are the same handle. Only whether the
247
+ // final directory exists decides anything, and only when it is created with an initializer
248
+ // file; otherwise a caller that accepts an empty directory needs no listing, which walks every
249
+ // key below the prefix and costs a large bucket seconds per level.
250
+ const createsInitializer = isFinalDirectory && optionsArg.createWithInitializerFile;
251
+ if ((optionsArg.getEmptyDirectory || optionsArg.createWithInitializerFile) && !createsInitializer) {
252
+ return new Directory(this.bucketRef, directoryArg, dirNameToSearch);
253
+ }
246
254
  const directories = await directoryArg.listDirectories();
247
255
  let returnDirectory = directories.find((directory) => {
248
256
  return directory.name === dirNameToSearch;