@procedures/mongo 0.35.0 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +27 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/module.d.ts +2 -2
  9. package/dist/module.d.ts.map +1 -1
  10. package/dist/module.js +4 -5
  11. package/dist/module.js.map +1 -1
  12. package/dist/procedures/input-schemas.d.ts.map +1 -1
  13. package/dist/procedures/input-schemas.js +29 -2
  14. package/dist/procedures/input-schemas.js.map +1 -1
  15. package/dist/storage/document-storage.d.ts +162 -3
  16. package/dist/storage/document-storage.d.ts.map +1 -1
  17. package/dist/storage/document-storage.js +325 -7
  18. package/dist/storage/document-storage.js.map +1 -1
  19. package/dist/storage/index.d.ts +3 -1
  20. package/dist/storage/index.d.ts.map +1 -1
  21. package/dist/storage/index.js +3 -1
  22. package/dist/storage/index.js.map +1 -1
  23. package/dist/storage/materializer.d.ts.map +1 -1
  24. package/dist/storage/materializer.js +7 -0
  25. package/dist/storage/materializer.js.map +1 -1
  26. package/dist/storage/own-writes.d.ts +51 -0
  27. package/dist/storage/own-writes.d.ts.map +1 -0
  28. package/dist/storage/own-writes.js +130 -0
  29. package/dist/storage/own-writes.js.map +1 -0
  30. package/dist/storage/storage-hints.d.ts +230 -0
  31. package/dist/storage/storage-hints.d.ts.map +1 -0
  32. package/dist/storage/storage-hints.js +876 -0
  33. package/dist/storage/storage-hints.js.map +1 -0
  34. package/package.json +3 -3
  35. package/src/index.ts +4 -0
  36. package/src/module.ts +5 -5
  37. package/src/procedures/input-schemas.ts +29 -2
  38. package/src/storage/document-storage.ts +394 -7
  39. package/src/storage/index.ts +4 -0
  40. package/src/storage/materializer.ts +7 -0
  41. package/src/storage/own-writes.ts +157 -0
  42. package/src/storage/storage-hints.ts +971 -0
@@ -92,9 +92,11 @@
92
92
  */
93
93
  import { randomUUID } from "node:crypto";
94
94
  import { isDeepStrictEqual } from "node:util";
95
+ import { BSON } from "mongodb";
95
96
  import { VersionConflictError, applyPatch, preconditionNotSupported, queryEntries, versionConflict, } from "@procedures/collections";
96
97
  import { optionalMs, toMs } from "@procedures/client";
97
98
  import { ensureConnection } from "../connection.js";
99
+ import { noteOwnNumbers, notePrunedIds } from "./own-writes.js";
98
100
  import { boundPipeline, cutResults, DEFAULT_PIPELINE_LIMITS, scopePipeline } from "./pipeline-scope.js";
99
101
  /** The fields of the storage in a stored document. */
100
102
  const KEY = "_key";
@@ -270,6 +272,58 @@ export function decodeStoredDocument(doc, options = {}) {
270
272
  const entries = Object.entries(form.partitionFields(partition));
271
273
  return { id, partition, item: itemOf(doc, id, entries, options.addId ?? true) };
272
274
  }
275
+ /**
276
+ * This function gives the version of a raw stored document of a `MongoDocumentStorage`: the
277
+ * version that `storage.versionOf(id)` gives for the same document (roadmap CLI-77). The storage
278
+ * uses it too, so the two never differ.
279
+ *
280
+ * - The version is the sequence number of the last write through the storage (`_seq`), as text.
281
+ * - A document without `_seq` (an old document, or one that a raw replace wrote) has the version `0`.
282
+ * - A tombstone, a value that is not a plain object, and a document whose id neither `idOf` nor
283
+ * `_key` tells give `undefined`: they are no items, as for `decodeStoredDocument`.
284
+ *
285
+ * @param doc - The stored document, for example the `fullDocument` of a change event
286
+ * @param form - The stored form of the storage, for example `storage.form` (default `DEFAULT_STORED_FORM`)
287
+ * @param partition - The partition of the document. The default is `form.partitionOf(doc)`.
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const version = versionOfStored(change.fullDocument, notes.form); // the same as await notes.versionOf(id)
292
+ * ```
293
+ */
294
+ export function versionOfStored(doc, form = DEFAULT_STORED_FORM, partition) {
295
+ if (!isPlainObject(doc) || doc[TOMB] === true)
296
+ return undefined;
297
+ const place = partition ?? form.partitionOf(doc);
298
+ if (typeof form.idOf(doc["_id"], place) !== "string" && typeof doc[KEY] !== "string")
299
+ return undefined;
300
+ return seqVersion(doc);
301
+ }
302
+ /** The version of the `_seq` of a stored document: the number as text, or `0` without it. */
303
+ function seqVersion(doc) {
304
+ const seq = doc[SEQ];
305
+ return typeof seq === "number" ? String(seq) : "0";
306
+ }
307
+ /** True when two values have the same BSON. */
308
+ function sameBson(a, b) {
309
+ try {
310
+ return Buffer.from(BSON.serialize({ v: a })).equals(Buffer.from(BSON.serialize({ v: b })));
311
+ }
312
+ catch {
313
+ return false;
314
+ }
315
+ }
316
+ /** The most floor documents that `partitions()` reads. */
317
+ const MAX_KNOWN_PARTITIONS = 10_000;
318
+ function escapeRegex(text) {
319
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
320
+ }
321
+ /** The reader of the parts of a storage. The class sets it, so it can read its private fields. */
322
+ let sourceOfStorage;
323
+ /** This function gives the parts of a storage that `watchStorageChanges` reads. The package does not export it. */
324
+ export function storageChangeSource(storage) {
325
+ return sourceOfStorage(storage);
326
+ }
273
327
  /** True for a plain object. */
274
328
  function isPlainObject(value) {
275
329
  if (typeof value !== "object" || value === null || Array.isArray(value))
@@ -405,7 +459,23 @@ export class MongoDocumentStorage {
405
459
  /** The fields that only the storage writes: its fields and the partition fields. */
406
460
  reserved;
407
461
  indexes;
462
+ /** True after this storage wrote (or found) the floor document of its partition (roadmap O66). A failed write leaves it false. */
463
+ floorNoted = false;
408
464
  closed = false;
465
+ static {
466
+ sourceOfStorage = (storage) => ({
467
+ db: () => storage.database(),
468
+ collection: storage.collectionName,
469
+ counters: storage.countersName,
470
+ form: storage.form,
471
+ partition: storage.partition,
472
+ adopt: (raw) => storage.adoptRaw(raw),
473
+ adoptAll: (raws) => storage.adoptRawAll(raws),
474
+ rawDocuments: (after, limit) => storage.rawDocuments(after, limit),
475
+ storedDocuments: (ids) => storage.storedDocuments(ids),
476
+ partitions: () => storage.knownPartitions(),
477
+ });
478
+ }
409
479
  constructor(options, partitions) {
410
480
  if (options.partition !== undefined && (options.partition === "" || options.partition.includes(SEPARATOR))) {
411
481
  throw new Error("A partition must be a text without the character U+001F");
@@ -648,9 +718,11 @@ export class MongoDocumentStorage {
648
718
  /**
649
719
  * This method takes numbers and writes with them. On a replica set, the two are one
650
720
  * transaction. Else the numbers stay pending until the write ends, and a late write moves its
651
- * documents to new numbers.
721
+ * documents to new numbers. With `own` false (an adoption of a raw write, D39), a watcher of this
722
+ * process does not take the numbers as its own.
652
723
  */
653
- async withNumbers(db, collection, count, write) {
724
+ async withNumbers(db, collection, count, write, own = true) {
725
+ const noteOwn = own ? noteOwnNumbers : () => undefined;
654
726
  const client = await this.transactionClient(db);
655
727
  if (client) {
656
728
  const session = client.startSession();
@@ -659,6 +731,8 @@ export class MongoDocumentStorage {
659
731
  const counter = (await db.collection(this.countersName).findOneAndUpdate({ _id: this.collectionName }, { $inc: { seq: count }, $setOnInsert: { epoch: randomUUID() } }, { upsert: true, returnDocument: "after", session }));
660
732
  if (!counter)
661
733
  throw new Error(`The sequence of ${this.collectionName} is not available`);
734
+ // A watcher of the collection in this process skips the events of these numbers (roadmap CLI-77)
735
+ noteOwn(db, this.collectionName, counter.seq - count + 1, count);
662
736
  return write(counter.seq - count + 1, session);
663
737
  });
664
738
  }
@@ -667,6 +741,7 @@ export class MongoDocumentStorage {
667
741
  }
668
742
  }
669
743
  let { first } = await this.allocate(db, count);
744
+ noteOwn(db, this.collectionName, first, count);
670
745
  let result;
671
746
  try {
672
747
  result = await write(first);
@@ -680,6 +755,7 @@ export class MongoDocumentStorage {
680
755
  throw new Error(`A write of ${this.collectionName} stayed late after ${MAX_MOVES} moves to new numbers`);
681
756
  // Late: a reader can have passed these numbers. The write takes new ones.
682
757
  const next = (await this.allocate(db, count)).first;
758
+ noteOwn(db, this.collectionName, next, count);
683
759
  await this.move(collection, first, count, next);
684
760
  first = next;
685
761
  }
@@ -722,6 +798,33 @@ export class MongoDocumentStorage {
722
798
  floorId() {
723
799
  return `floor${SEPARATOR}${this.collectionName}${SEPARATOR}${this.partition ?? ""}`;
724
800
  }
801
+ /**
802
+ * This method writes the floor document of the partition once for this storage, before its first
803
+ * write (roadmap O66). Then `knownPartitions()` of each process finds the partition, also without
804
+ * a prune, and a gap of `watchStorageChanges()` sends it a reset hint.
805
+ *
806
+ * - The floor `0` is neutral: `getChangesSince` gives a full result only for a version below the
807
+ * floor, and a version is never below `0`. A missing floor document also reads as `0`. So the
808
+ * document changes no result of the change log.
809
+ * - `$setOnInsert` writes only a new document: a floor of a prune (`$max`) stays as it is.
810
+ * - A duplicate key (another process inserted the same document first) is a success.
811
+ * - The upsert runs in the queue of the writes, before the task, so never in the transaction of
812
+ * `withNumbers()`: it adds no write conflict to the transaction of the user, and a transaction
813
+ * that tries again does not write it again.
814
+ * - A failure does not fail the write of the user. The flag stays false, so the next write tries again.
815
+ */
816
+ async noteFloor(db) {
817
+ if (this.floorNoted)
818
+ return;
819
+ try {
820
+ await db.collection(this.countersName).updateOne({ _id: this.floorId() }, { $setOnInsert: { floor: 0 } }, { upsert: true });
821
+ this.floorNoted = true;
822
+ }
823
+ catch (error) {
824
+ if (isDuplicateKey(error))
825
+ this.floorNoted = true;
826
+ }
827
+ }
725
828
  /** This method drops the oldest tombstones of the partition over the limit, and keeps the floor of the log. */
726
829
  async pruneTombstones(db, items) {
727
830
  const count = await items.countDocuments({ ...this.partitionFilter(), [TOMB]: true });
@@ -735,6 +838,8 @@ export class MongoDocumentStorage {
735
838
  .toArray();
736
839
  if (oldest.length === 0)
737
840
  return;
841
+ // A watcher of this process reads the delete of a pruned tombstone without the image before the change (roadmap CLI-77)
842
+ notePrunedIds(db, this.collectionName, oldest.map((doc) => doc._id));
738
843
  await items.deleteMany({ _id: { $in: oldest.map((doc) => doc._id) } });
739
844
  const floor = Math.max(...oldest.map((doc) => Number(doc[SEQ])));
740
845
  await db.collection(this.countersName).updateOne({ _id: this.floorId() }, { $max: { floor } }, { upsert: true });
@@ -848,14 +953,14 @@ export class MongoDocumentStorage {
848
953
  }
849
954
  /**
850
955
  * The version of an item: the sequence number of its last write, or undefined for a missing id.
851
- * An item without a write through the storage (no `_seq`) has the version `0`.
956
+ * An item without a write through the storage (no `_seq`) has the version `0`. It is the version
957
+ * of `versionOfStored()` of the stored document (roadmap CLI-77): the two share one rule.
852
958
  */
853
959
  async versionOf(id) {
854
960
  const { items } = await this.collection();
855
961
  const doc = await items.findOne(this.liveFilter({ _id: this.storedId(id) }), { projection: { [SEQ]: 1 } });
856
- if (!doc)
857
- return undefined;
858
- return typeof doc[SEQ] === "number" ? String(doc[SEQ]) : "0";
962
+ // The filter finds the document by the stored `_id` of the id in the partition: it is the item
963
+ return doc ? seqVersion(doc) : undefined;
859
964
  }
860
965
  async getChangesSince(since) {
861
966
  const { db, items } = await this.collection();
@@ -900,7 +1005,11 @@ export class MongoDocumentStorage {
900
1005
  // ===========================================================================
901
1006
  async write(task) {
902
1007
  const { db, items } = await this.collection();
903
- return enqueue(db, this.collectionName, () => task(db, items));
1008
+ return enqueue(db, this.collectionName, async () => {
1009
+ // The floor document of the partition, outside each transaction of the task (roadmap O66)
1010
+ await this.noteFloor(db);
1011
+ return task(db, items);
1012
+ });
904
1013
  }
905
1014
  async set(id, value, options) {
906
1015
  await this.setBatch([[id, value]], options?.expectedVersion === undefined ? undefined : { expectedVersions: [[id, options.expectedVersion]] });
@@ -1024,6 +1133,215 @@ export class MongoDocumentStorage {
1024
1133
  });
1025
1134
  await this.pruneTombstones(db, collection);
1026
1135
  }
1136
+ // ===========================================================================
1137
+ // The raw writes (D39, roadmap CLI-77)
1138
+ // ===========================================================================
1139
+ /**
1140
+ * This method adopts a raw write: one conditional write through the path of each write gives the
1141
+ * document a new `_seq` (see `RawStorageWrite`). It is for `watchStorageChanges()`, and it is no
1142
+ * general setter of `_seq`.
1143
+ */
1144
+ async adoptRaw(raw) {
1145
+ const [result] = await this.adoptRawAll([raw]);
1146
+ if (result === undefined || typeof result !== "string")
1147
+ throw result?.error;
1148
+ return result;
1149
+ }
1150
+ /**
1151
+ * This method adopts several raw writes. The writes of each partition take one block of numbers
1152
+ * through the path of each write. A number of a write that does not win stays unused.
1153
+ */
1154
+ async adoptRawAll(raws) {
1155
+ const results = raws.map(() => "unknown");
1156
+ const groups = new Map();
1157
+ raws.forEach((raw, index) => {
1158
+ const target = this.adoptionTarget(raw);
1159
+ if (!target)
1160
+ return;
1161
+ let group = groups.get(target.storage);
1162
+ if (!group)
1163
+ groups.set(target.storage, (group = []));
1164
+ group.push({ index, raw, id: target.id });
1165
+ });
1166
+ for (const [storage, group] of groups) {
1167
+ try {
1168
+ const done = await storage.adoptGroup(group);
1169
+ group.forEach((entry, position) => (results[entry.index] = done[position] ?? "lost"));
1170
+ }
1171
+ catch (error) {
1172
+ for (const entry of group)
1173
+ results[entry.index] = { error };
1174
+ }
1175
+ }
1176
+ return results;
1177
+ }
1178
+ /** The adoption of raw writes of this partition, with one block of numbers. */
1179
+ async adoptGroup(group) {
1180
+ return this.write(async (db, collection) => {
1181
+ const results = await this.withNumbers(db, collection, group.length, async (first, session) => {
1182
+ const out = [];
1183
+ for (const [position, { raw, id }] of group.entries()) {
1184
+ const seq = first + position;
1185
+ out.push(raw.after === undefined ? await this.adoptDelete(collection, raw, id, seq, session) : await this.adoptWrite(collection, raw, raw.after, id, seq, session));
1186
+ }
1187
+ return out;
1188
+ }, false);
1189
+ if (results.some((result, position) => result === "adopted" && group[position].raw.after === undefined))
1190
+ await this.pruneTombstones(db, collection);
1191
+ return results;
1192
+ });
1193
+ }
1194
+ /** A raw hard delete: a tombstone with the id and the partition of the image before the change. */
1195
+ async adoptDelete(collection, raw, id, seq, session) {
1196
+ // In a transaction, a duplicate key would end the transaction: the read comes first
1197
+ if (session && (await collection.findOne({ _id: raw.storedId }, { projection: { _id: 1 }, session })))
1198
+ return "lost";
1199
+ const tomb = { _id: raw.storedId, [KEY]: id, [SEQ]: seq, [TOMB]: true };
1200
+ this.putPartition(tomb);
1201
+ try {
1202
+ await collection.insertOne(tomb, session ? { session } : {});
1203
+ return "adopted";
1204
+ }
1205
+ catch (error) {
1206
+ // A raw insert of the same _id came after the delete: that insert is the newer state
1207
+ if (isDuplicateKey(error))
1208
+ return "lost";
1209
+ throw error;
1210
+ }
1211
+ }
1212
+ /** A raw insert, replace or update: one conditional write on the exact state of the event. */
1213
+ async adoptWrite(collection, raw, after, id, seq, session) {
1214
+ const fields = this.restoredFields(raw, after, id);
1215
+ // The exact state of the event: the same `_seq` (or none), and no field that the adoption sets
1216
+ const filter = { _id: raw.storedId, [SEQ]: raw.seq ?? { $exists: false } };
1217
+ for (const name of Object.keys(fields))
1218
+ filter[name] = { $exists: false };
1219
+ const result = await collection.updateOne(filter, { $set: { ...fields, [SEQ]: seq } }, session ? { session } : {});
1220
+ return result.matchedCount === 1 ? "adopted" : "lost";
1221
+ }
1222
+ /** One page of the documents without `_seq`, in the order of `_id` (the sweep of `watchStorageChanges()`). */
1223
+ async rawDocuments(after, limit) {
1224
+ const db = await this.database();
1225
+ // The scan walks the index of _id from the last _id: `min` bounds the scan, in the order of the
1226
+ // BSON types. `min` includes that _id, so the page skips it. No index can find the documents
1227
+ // without a field (a partial index cannot express $exists: false): the scan reads each document.
1228
+ let cursor = db.collection(this.collectionName).find({ [SEQ]: { $exists: false } }).hint({ _id: 1 });
1229
+ if (after)
1230
+ cursor = cursor.min({ _id: after.id });
1231
+ const docs = await cursor.limit(after ? limit + 1 : limit).toArray();
1232
+ if (after && docs.length > 0 && sameBson(docs[0]["_id"], after.id))
1233
+ docs.shift();
1234
+ return docs.slice(0, limit);
1235
+ }
1236
+ /** The stored documents of these stored `_id`s, for the adoptions that `watchStorageChanges()` does again. */
1237
+ async storedDocuments(ids) {
1238
+ const db = await this.database();
1239
+ const filter = { _id: { $in: [...ids] } };
1240
+ return db.collection(this.collectionName).find(filter).toArray();
1241
+ }
1242
+ /**
1243
+ * The storage of the partition of a raw write and the id of its document, or undefined when
1244
+ * neither the documents of the event nor the stored `_id` tell them. The partition comes from
1245
+ * `form.partitionOf()` of the image before the change, else of the document after the write,
1246
+ * else of the stored `_id` (the order of `storageChangeOf()`). The document must not have a
1247
+ * partition field with another value.
1248
+ */
1249
+ adoptionTarget(raw) {
1250
+ const doc = raw.after ?? raw.before;
1251
+ if (!doc)
1252
+ return undefined;
1253
+ const places = new Set();
1254
+ for (const source of [raw.before, raw.after, { _id: raw.storedId }])
1255
+ if (isPlainObject(source))
1256
+ places.add(this.form.partitionOf(source));
1257
+ for (const place of places) {
1258
+ if (place !== this.partition && (this.partition !== undefined || place === undefined))
1259
+ continue;
1260
+ let storage;
1261
+ try {
1262
+ storage = place === this.partition ? this : this.forPartition(place);
1263
+ }
1264
+ catch {
1265
+ continue;
1266
+ }
1267
+ if (!storage.holds(doc))
1268
+ continue;
1269
+ const fromId = this.form.idOf(raw.storedId, place);
1270
+ const key = raw.after?.[KEY] ?? raw.before?.[KEY];
1271
+ const id = typeof fromId === "string" ? fromId : typeof key === "string" ? key : undefined;
1272
+ if (id === undefined || id.includes(SEPARATOR))
1273
+ continue;
1274
+ return { storage, id };
1275
+ }
1276
+ return undefined;
1277
+ }
1278
+ /** True when each partition field of a document has the value of this partition, or is absent (a raw replace can drop it). */
1279
+ holds(doc) {
1280
+ for (const [name, value] of this.partitionEntries) {
1281
+ if (!(name in doc))
1282
+ continue;
1283
+ if (value === undefined || !isDeepStrictEqual(doc[name], value))
1284
+ return false;
1285
+ }
1286
+ return true;
1287
+ }
1288
+ /**
1289
+ * The storage fields and the partition fields that a raw write took from the document, for the
1290
+ * adoption to set again. No field of the item is in it.
1291
+ *
1292
+ * - The partition fields of this partition that the document does not have.
1293
+ * - `_key` (with `_kid`) of the image before the change. Without the image, `_key` only when
1294
+ * `form.idOf()` does not tell the id, with `_kid`: the item keeps its `_id`, as a document from
1295
+ * outside the storage.
1296
+ * - `_ord` of the image before the change, else `0`, the place of a document without `_ord`
1297
+ * (the rule of the old documents).
1298
+ * - The names of `_kpf` of the image before the change that the document has with the value of
1299
+ * the partition.
1300
+ */
1301
+ restoredFields(raw, after, id) {
1302
+ const before = raw.before;
1303
+ const fields = {};
1304
+ for (const [name, value] of this.partitionEntries)
1305
+ if (value !== undefined && !(name in after))
1306
+ fields[name] = value;
1307
+ if (after[KEY] === undefined) {
1308
+ if (typeof before?.[KEY] === "string") {
1309
+ fields[KEY] = before[KEY];
1310
+ if (before[KEEP_ID] === true && after[KEEP_ID] === undefined)
1311
+ fields[KEEP_ID] = true;
1312
+ }
1313
+ else if (this.form.idOf(raw.storedId, this.partition) !== id) {
1314
+ fields[KEY] = id;
1315
+ if (after[KEEP_ID] === undefined)
1316
+ fields[KEEP_ID] = true;
1317
+ }
1318
+ }
1319
+ if (after[ORD] === undefined)
1320
+ fields[ORD] = typeof before?.[ORD] === "number" ? before[ORD] : 0;
1321
+ const kept = before?.[KEEP_PART];
1322
+ if (after[KEEP_PART] === undefined && Array.isArray(kept)) {
1323
+ const names = kept.filter((name) => typeof name === "string" && this.partitionValues.has(name) && isDeepStrictEqual(after[name], this.partitionValues.get(name)));
1324
+ if (names.length > 0)
1325
+ fields[KEEP_PART] = names;
1326
+ }
1327
+ return fields;
1328
+ }
1329
+ /** The partitions that the storage knows: the floor documents of the counters (of the first writes and of the prunes), and the storages of `forPartition()`. */
1330
+ async knownPartitions() {
1331
+ const db = await this.database();
1332
+ const prefix = `floor${SEPARATOR}${this.collectionName}${SEPARATOR}`;
1333
+ const floors = await db
1334
+ .collection(this.countersName)
1335
+ .find({ _id: { $regex: `^${escapeRegex(prefix)}` } }, { projection: { _id: 1 } })
1336
+ .limit(MAX_KNOWN_PARTITIONS)
1337
+ .toArray();
1338
+ const found = new Set(this.partitions.keys());
1339
+ for (const floor of floors) {
1340
+ const rest = String(floor._id).slice(prefix.length);
1341
+ found.add(rest === "" ? undefined : rest);
1342
+ }
1343
+ return [...found];
1344
+ }
1027
1345
  /**
1028
1346
  * The preconditions of a write (roadmap CLI-16), after a check of their ids. A storage without
1029
1347
  * `versionOf` throws a `TypeError` before the write starts.