@lossless.org/client 1.22.2 → 1.23.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 (32) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.bootstraprecord.js +4 -4
  3. package/dist_ts/nosqldb/classes.connection.d.ts +3 -0
  4. package/dist_ts/nosqldb/classes.connection.js +3 -2
  5. package/dist_ts/nosqldb/classes.db.d.ts +31 -0
  6. package/dist_ts/nosqldb/classes.db.js +41 -1
  7. package/dist_ts/nosqldb/classes.doc.js +5 -5
  8. package/dist_ts/nosqldb/classes.exactpersistence.d.ts +3 -2
  9. package/dist_ts/nosqldb/classes.exactpersistence.js +8 -7
  10. package/dist_ts/nosqldb/classes.session.d.ts +7 -6
  11. package/dist_ts/nosqldb/classes.session.js +88 -36
  12. package/dist_ts/nosqldb/classes.writelog.d.ts +129 -0
  13. package/dist_ts/nosqldb/classes.writelog.js +497 -0
  14. package/dist_ts/nosqldb/classes.writeobserver.d.ts +13 -0
  15. package/dist_ts/nosqldb/classes.writeobserver.js +47 -25
  16. package/dist_ts/nosqldb/classes.writerevisions.d.ts +40 -4
  17. package/dist_ts/nosqldb/classes.writerevisions.js +174 -37
  18. package/dist_ts/nosqldb/engineidentity.d.ts +6 -0
  19. package/dist_ts/nosqldb/engineidentity.js +14 -1
  20. package/package.json +2 -2
  21. package/readme.md +11 -10
  22. package/ts/00_commitinfo_data.ts +1 -1
  23. package/ts/nosqldb/classes.bootstraprecord.ts +3 -3
  24. package/ts/nosqldb/classes.connection.ts +5 -1
  25. package/ts/nosqldb/classes.db.ts +64 -0
  26. package/ts/nosqldb/classes.doc.ts +4 -4
  27. package/ts/nosqldb/classes.exactpersistence.ts +8 -7
  28. package/ts/nosqldb/classes.session.ts +107 -38
  29. package/ts/nosqldb/classes.writelog.ts +701 -0
  30. package/ts/nosqldb/classes.writeobserver.ts +62 -28
  31. package/ts/nosqldb/classes.writerevisions.ts +225 -41
  32. package/ts/nosqldb/engineidentity.ts +16 -0
@@ -8,6 +8,11 @@ export const unidentifiedSmartdataEngine = Object.freeze({
8
8
  * answering with the literal field paths: @lossless.org/nosqldb 10.5.0.
9
9
  */
10
10
  const compositeGroupingEngineRelease = '10.5.0';
11
+ /**
12
+ * The NoSQLDB release that publishes its durable commit journal through the
13
+ * `getCommittedWrites` command: @lossless.org/nosqldb 10.9.0.
14
+ */
15
+ const commitJournalEngineRelease = '10.9.0';
11
16
  const parseEngineVersion = (versionArg) => {
12
17
  const parts = versionArg.split('.');
13
18
  if (parts.length !== 3) {
@@ -57,6 +62,14 @@ export const smartdataCompositeGrouping = (identityArg) => {
57
62
  return 'unsupported';
58
63
  }
59
64
  };
65
+ /**
66
+ * Whether the identified engine publishes a durable, commit-ordered journal of
67
+ * the transactions it committed. Only NoSQLDB does, from 10.9.0 on; MongoDB's
68
+ * change streams are a different contract and are not used for it.
69
+ */
70
+ export const smartdataCommitJournal = (identityArg) => identityArg.product === 'NoSQLDB'
71
+ && identityArg.version !== null
72
+ && engineVersionReaches(identityArg.version, commitJournalEngineRelease);
60
73
  export const composeSmartdataCapabilities = (backendArg, compositeGroupingArg) => {
61
74
  const capabilities = {
62
75
  backend: backendArg,
@@ -102,4 +115,4 @@ export const probeSmartdataEngineIdentity = async (dbArg, budgetArg) => {
102
115
  return unidentifiedSmartdataEngine;
103
116
  }
104
117
  };
105
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW5naW5laWRlbnRpdHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9ub3NxbGRiL2VuZ2luZWlkZW50aXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBdUJ4QyxNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBNkIsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUNqRixPQUFPLEVBQUUsU0FBUztJQUNsQixPQUFPLEVBQUUsSUFBSTtDQUNkLENBQUMsQ0FBQztBQUVIOzs7R0FHRztBQUNILE1BQU0sOEJBQThCLEdBQUcsUUFBUSxDQUFDO0FBRWhELE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxVQUFrQixFQUFpQyxFQUFFO0lBQy9FLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDcEMsSUFBSSxLQUFLLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3ZCLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFDRCxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FDcEMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUNyRCxDQUFDO0lBQ0YsT0FBTyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsU0FBUyxFQUFFLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUM7QUFDN0YsQ0FBQyxDQUFDO0FBRUY7Ozs7R0FJRztBQUNILE1BQU0sb0JBQW9CLEdBQUcsQ0FBQyxVQUFrQixFQUFFLFVBQWtCLEVBQVcsRUFBRTtJQUMvRSxN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
118
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW5naW5laWRlbnRpdHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9ub3NxbGRiL2VuZ2luZWlkZW50aXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBdUJ4QyxNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBNkIsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUNqRixPQUFPLEVBQUUsU0FBUztJQUNsQixPQUFPLEVBQUUsSUFBSTtDQUNkLENBQUMsQ0FBQztBQUVIOzs7R0FHRztBQUNILE1BQU0sOEJBQThCLEdBQUcsUUFBUSxDQUFDO0FBRWhEOzs7R0FHRztBQUNILE1BQU0sMEJBQTBCLEdBQUcsUUFBUSxDQUFDO0FBRTVDLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxVQUFrQixFQUFpQyxFQUFFO0lBQy9FLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDcEMsSUFBSSxLQUFLLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3ZCLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFDRCxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FDcEMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUNyRCxDQUFDO0lBQ0YsT0FBTyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsU0FBUyxFQUFFLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUM7QUFDN0YsQ0FBQyxDQUFDO0FBRUY7Ozs7R0FJRztBQUNILE1BQU0sb0JBQW9C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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lossless.org/client",
3
- "version": "1.22.2",
3
+ "version": "1.23.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "registry": "https://verdaccio.lossless.digital"
@@ -88,7 +88,7 @@
88
88
  "@git.zone/tsrun": "3.0.3",
89
89
  "@git.zone/tstest": "7.2.0",
90
90
  "@lossless.org/nosqldb": "8.0.2",
91
- "@lossless.org/nosqldb-qualified": "npm:@lossless.org/nosqldb@10.7.2",
91
+ "@lossless.org/nosqldb-qualified": "npm:@lossless.org/nosqldb@10.9.0",
92
92
  "@lossless.org/objectstorage": "10.3.1",
93
93
  "@lossless.org/sqldb": "0.2.3",
94
94
  "@push.rocks/qenv": "^6.1.3",
package/readme.md CHANGED
@@ -119,13 +119,14 @@ Family imports preserve the established SmartData and SmartBucket names and cons
119
119
  | Grouped totals over nested paths | `getGroupedTotals()` / `getGroupedTotalsPage()` group by and sum nested paths declared through `atomicPaths` or `arrayPaths`, and `unwind: path` groups by each element of one declared array |
120
120
  | Grouped totals over array elements | with `unwind: 'lines'`, `groupBy` and `sums` name declared fields of the unwound element — `groupBy: ['lines.accountNumber', 'lines.side']`, `sums: ['lines.amountCents']` — so per-account debit and credit totals need no raw aggregation; undeclared element fields and an `unwind` that holds no `groupBy` or `sums` path stay refused with `invalid_argument` |
121
121
  | Grouped totals element filter | `elementFilter: { 'lines.accountNumber': '1200' }` beside `unwind: 'lines'` counts and sums only the matching elements — one account's balance per side — with the atomic-filter operators on the unwound array and its declared element paths; any other path or operator is refused with `invalid_argument` |
122
- | Grouped totals paging | rows come ordered by group key; `getGroupedTotalsPage()` returns `nextCursor` while `hasMore`, `cursor` continues after it, and `iterateGroupedTotals()` reads every group page by page — exact across pages for null, number, string and boolean group values (proven on NoSQLDB 8.0.2, 10.7.2 and MongoDB; ObjectId and date values are ordered by BSON type order but not yet covered by the paging tests), refused with `unsupported_operation` for other values, and a cursor from another query is refused with `invalid_argument` |
122
+ | Grouped totals paging | rows come ordered by group key; `getGroupedTotalsPage()` returns `nextCursor` while `hasMore`, `cursor` continues after it, and `iterateGroupedTotals()` reads every group page by page — exact across pages for null, number, string and boolean group values (proven on NoSQLDB 8.0.2, 10.9.0 and MongoDB; ObjectId and date values are ordered by BSON type order but not yet covered by the paging tests), refused with `unsupported_operation` for other values, and a cursor from another query is refused with `invalid_argument` |
123
123
  | Query-plan inspection | `Model.inspectQueryPlan(db, { filter, sort?, projection?, limit?, timeoutMS?, signal? })` — the winning plan's stages (`stage`, `depth`, `indexName`, `direction`) and `nReturned`/`totalKeysExamined`/`totalDocsExamined`, read-only and bounded, so a consumer proves a model query uses its intended index without raw driver access |
124
124
  | Per-instance model binding | `bindDocToManager(manager, Model)` returns a subclass of a model declared with `@managed()` and no manager argument, named like the model and bound to that manager, so several databases use the same models at once; any other binding is refused with `invalid_configuration` |
125
125
  | Subclasses need their own binding | a subclass of a bound model that declares decorators or a persistence policy of its own and no `@Collection()`, `@managed()` or `defineCollectionModel()` binding is refused with `invalid_configuration` on every read and write, so it never writes through the parent's schema; an undecorated subclass keeps the inherited binding |
126
126
  | Observing this process's writes | `db.observeWrites({ fields, listener })` — one frozen report per acknowledged model write, `{ collection, kind, documents, anchors }` with the requested top-level fields of the written documents and of the selector's equality anchors; writes inside a `db.createSession()` transaction are reported after the commit, never after a rollback; works on NoSQLDB, where `watch()` is unsupported |
127
127
  | Following this process's reads | `db.trackReads(fn)` resolves `{ result, reads }`; `reads.changed` turns true once a write this process makes through the database's models could have changed what a model read of `fn` returned, judged by the read's query and the ids it returned, and a write it cannot place counts as one that did, so a cache keyed by `reads` rebuilds only what a write touched; works on NoSQLDB |
128
- | Following every process's writes | with `new SmartdataDb({ ..., writeRevisions: { partitionField: 'organizationId' } })` in every writer, the awaited `reads.isCurrent()` counts changes per collection and partition in the write's transaction; confirmed writes of this database that leave `reads.changed` false receive exact revision allowances, while other writers and unknown outcomes stay conservative; a revision snapshot overlapping a local commit that touches its keys is unknowable; `db.invalidateTrackedReads()` states raw writes and migrations; needs an engine identified as NoSQLDB, elsewhere `isCurrent()` is always false |
128
+ | Following every process's writes | with `new SmartdataDb({ ..., writeRevisions: { partitionField: 'organizationId' } })` in every writer, the awaited `reads.isCurrent()` counts changes per collection and partition in the write's transaction; confirmed writes of this database that leave `reads.changed` false receive exact revision allowances, while other writers and unknown outcomes stay conservative; a revision snapshot overlapping a local commit that touches its keys is unknowable; `db.invalidateTrackedReads()` states raw writes and migrations; overlapping transactions of one process bump separate documents of a partition's counter, so they rarely conflict on it, a check reads the counters in one query plus one for those with shards in use, and every process of a database upgrades together; needs an engine identified as NoSQLDB, elsewhere `isCurrent()` is always false |
129
+ | Following committed writes in commit order | with `new SmartdataDb({ ..., writeLog: { fields: ['id', 'organizationId'] } })` in every writer, `db.followWrites({ fields, onWrite, onGap })` delivers each committed transaction once with its write reports: this instance's own commits at once (`origin: 'local'`), every other commit from the engine's commit journal in commit order (`origin: 'journal'`, with its `stamp`), within 250 ms while active and a second when idle; rollbacks and aborted retry attempts deliver nothing, raw writes name their collections in `unplaced`, and a follower that falls out of the journal's 15-minute retention or meets a restarted memory engine calls `onGap` and continues from the head; needs NoSQLDB 10.9.0, elsewhere the option is inert and following is refused with `unsupported_operation` |
129
130
  | Disposable test databases on a unix socket | `SmartdataTestDatabase.create({ connection: { socketPath } })` from `@lossless.org/client/testsupport` reaches an engine that listens only on a unix-domain socket, fault helpers included; the loopback `host`/`port` form is unchanged |
130
131
 
131
132
  `docs/source/smartdata/readme.md` documents each one with its refusals.
@@ -147,24 +148,24 @@ A bucket lookup sends one HeadBucket for the named bucket, never ListBuckets, so
147
148
 
148
149
  ### Document APIs that need a MongoDB backend
149
150
 
150
- The document model runs on both NoSQLDB and MongoDB, but several APIs do not mean the same thing on both. The NoSQLDB engine is qualified at 8.0.2 and 10.7.2 (see [Verification](#verification)); the two releases part on the grouped-totals and guarded-`$expr` rows below — 10.7.2 groups composite keys exactly and evaluates the guard, 8.0.2 does neither — on whether a migration can create a partial index or a collection validator at all, on when a transaction learns of a write conflict, and on whether `$elemMatch` applies query operators to scalar array elements.
151
+ The document model runs on both NoSQLDB and MongoDB, but several APIs do not mean the same thing on both. The NoSQLDB engine is qualified at 8.0.2 and 10.9.0 (see [Verification](#verification)); the two releases part on the grouped-totals and guarded-`$expr` rows below — 10.9.0 groups composite keys exactly and evaluates the guard, 8.0.2 does neither — on whether a migration can create a partial index or a collection validator at all, on when a transaction learns of a write conflict, and on whether `$elemMatch` applies query operators to scalar array elements.
151
152
 
152
153
  `init()` asks the connected engine what it is with one `buildInfo` command and records the answer on the database as `engineIdentity` (`{ product, version }`). NoSQLDB answers with its own `nosqldb` identity document from 10.3.0 onward; a MongoDB server answers `buildInfo` without one; an engine that refuses the command — NoSQLDB 8.0.2 answers `CommandNotFound` — stays `{ product: 'unknown', version: null }`. The identity is what states `capabilities` on a plain `SmartdataDb`, and an unidentified engine states none at all, so every capability-gated call refuses. `NoSqlConnection` keeps stating the capabilities of its declared backend from construction on.
153
154
 
154
155
  | Document API | On the NoSQLDB engine | On MongoDB |
155
156
  | --- | --- | --- |
156
- | `getGroupedTotals()` / `getGroupedTotalsPage()` / `iterateGroupedTotals()` with one `groupBy` field | exact, including `sums`, declared nested paths, `unwind` of a declared array and declared fields of its element, `elementFilter`, group key order and `cursor` paging, on 8.0.2 and 10.7.2 alike | exact |
157
+ | `getGroupedTotals()` / `getGroupedTotalsPage()` / `iterateGroupedTotals()` with one `groupBy` field | exact, including `sums`, declared nested paths, `unwind` of a declared array and declared fields of its element, `elementFilter`, group key order and `cursor` paging, on 8.0.2 and 10.9.0 alike | exact |
157
158
  | the same with **two** `groupBy` fields | exact, `sums`, nested paths, `unwind`, two element fields of one unwound array (`lines.accountNumber` with `lines.side`), `elementFilter` and `cursor` paging included, on an engine that identifies itself as NoSQLDB 10.5.0 or newer: that release evaluates the composite group key per document. Below it the call is refused with `unsupported_operation` naming both fields and the single-field form, because the engine leaves the composite group key unresolved and answers with a single row whose group values are the literal field paths (`'$owner'`) and whose count is the whole filtered collection; the refusal is the guard against that wrong number, not a fix for it | exact; the client wraps each key member in `$ifNull`, because MongoDB leaves a missing member out of the key and would otherwise report it as a second null group |
158
159
  | guarded `$expr` atomic filters (`$add`, `$lte`, `$ifNull`) | exact on 10.6.0 and newer: the engine evaluates the client's fail-closed compilation (`$cond`, `$let`, `$isNumber`, `$type`), so the guard admits within the limit, refuses beyond it and never matches a stored value that is not a finite number. 8.0.2 rejects the compilation with `invalid argument: $cond` and leaves the document untouched | exact |
159
160
  | `watch()` change streams | unsupported. `NoSqlConnection` refuses with `unsupported_capability`, and so does a plain `SmartdataDb` whose engine identified itself; against an engine that answers no identity the refusal instead arrives from the engine as `CommandNotFound` once the change stream is read. `db.observeWrites()` reports the process's own model writes on both backends | supported on replica sets |
160
- | model-declared partial indexes (`partialFilterExpression`) | not expressible: the index-option type rejects it and the runtime refuses it with `invalid_configuration`. A client decision, not an engine limit on every release — 10.6.0 and newer create partial indexes, 8.0.2 refuses the option with `InvalidOptions` (72) — because one model declaration has to hold on every qualified backend. A migration may create one on 10.7.2; `toleratedIndexNames` accepts it and inspection reports its filter | not expressible, but a migration may own one; inspection reports its filter |
161
+ | model-declared partial indexes (`partialFilterExpression`) | not expressible: the index-option type rejects it and the runtime refuses it with `invalid_configuration`. A client decision, not an engine limit on every release — 10.6.0 and newer create partial indexes, 8.0.2 refuses the option with `InvalidOptions` (72) — because one model declaration has to hold on every qualified backend. A migration may create one on 10.9.0; `toleratedIndexNames` accepts it and inspection reports its filter | not expressible, but a migration may own one; inspection reports its filter |
161
162
  | collection validators (`validator`, `validationLevel`, `validationAction`) | no client API; a migration owns a validator through the driver. 10.7.0 and newer create the collection, list the options exactly as given and enforce them on every model write: a violating `insert()` or `atomicUpdate()` rejects with the engine's `DocumentValidationFailure` (121) and its `errInfo`, which the client passes through unchanged rather than mapping to a `SmartdataPersistenceError` code, and stores nothing. Models initialize on a validated collection like on any other. 8.0.2 refuses the option with `InvalidOptions` (72) and creates no collection, so a validator is never silently dropped | enforced; not exercised by the client's MongoDB suite |
162
- | write conflicts between transactions, and writes outside a transaction to a document a transaction holds | 10.7.2 (since 10.6.1) detects a conflict at the statement, as MongoDB does: the first writer wins, and the later transaction's statement rejects with the engine's `WriteConflict` (112) labelled `TransientTransactionError`, which the client passes through so `session.withTransaction()` re-runs that callback. A write outside a transaction waits until the transaction holding its document ends, then applies on top; the wait is bounded by the write's own `timeoutMS` and by the engine's fixed 60-second transaction lifetime, after which a waiting writer aborts the holder. Awaiting such a write inside the holding transaction's callback therefore stalls that transaction until one of the two expires. 8.0.2 detects the conflict at commit: the first transaction to commit wins, a write outside a transaction applies at once, and the transaction that wrote first re-runs. Both writes land on either engine; a callback must be safe to re-run and must not swallow the statement error | first writer wins at the statement and outside writes wait, MongoDB's documented rule; not exercised by the client's MongoDB suite |
163
+ | write conflicts between transactions, and writes outside a transaction to a document a transaction holds | 10.9.0 (since 10.6.1) detects a conflict at the statement, as MongoDB does: the first writer wins, and the later transaction's statement rejects with the engine's `WriteConflict` (112) labelled `TransientTransactionError`, which the client passes through so `session.withTransaction()` re-runs that callback. A write outside a transaction waits until the transaction holding its document ends, then applies on top; the wait is bounded by the write's own `timeoutMS` and by the engine's fixed 60-second transaction lifetime, after which a waiting writer aborts the holder. Awaiting such a write inside the holding transaction's callback therefore stalls that transaction until one of the two expires. 8.0.2 detects the conflict at commit: the first transaction to commit wins, a write outside a transaction applies at once, and the transaction that wrote first re-runs. Both writes land on either engine; a callback must be safe to re-run and must not swallow the statement error | first writer wins at the statement and outside writes wait, MongoDB's documented rule; not exercised by the client's MongoDB suite |
163
164
  | value-form `$elemMatch` in read filters (`{ scores: { $elemMatch: { $gte: 80, $lt: 90 } } }`, `{ labels: { $not: { $elemMatch: { $nin: runnerLabels } } } }`) | exact on 10.7.1 and newer: the operators apply to each array element as a whole, so the range matches only a document with one score inside it and the claim selects a job only when every one of its labels is a runner label. 8.0.2 parses every operand as a query over embedded documents and refuses the value form with `TypeMismatch` (14). A non-document operand is refused on both, with MongoDB's `BadValue` (2) on 10.7.1 and newer and 14 on 8.0.2. The client passes the operand through unchanged; the filter type admits the value form over scalar elements and the document form over embedded documents. Atomic filters (`atomicUpdate()` and the other atomic writes) accept no `$elemMatch` on any backend: the client refuses it with `invalid_argument` before any backend is contacted | exact |
164
- | `Model.inspectQueryPlan()` | explains on 10.7.2: the summary names the winning plan's stages and its execution totals. 8.0.2 answers `explain` with `CommandNotFound` (59), and the call rejects with `unsupported_operation` ("The connected engine does not explain queries.") rather than returning a plan it cannot know. Stage shapes are the engine's own: on 10.7.2 a limited sort on an unindexed field is `LIMIT` over `SORT` over `COLLSCAN`, an equality on a unique single-field index is `FETCH` over `IXSCAN`, and an `$or` whose branches each have an index is a `COLLSCAN` | explains: the same limited sort is `SORT` over `COLLSCAN`, the limit folded into the sort; the equality is `EXPRESS_IXSCAN` on the unique index; the `$or` is `SUBPLAN` over `FETCH` over `OR` over one `IXSCAN` per branch |
165
+ | `Model.inspectQueryPlan()` | explains on 10.9.0: the summary names the winning plan's stages and its execution totals. 8.0.2 answers `explain` with `CommandNotFound` (59), and the call rejects with `unsupported_operation` ("The connected engine does not explain queries.") rather than returning a plan it cannot know. Stage shapes are the engine's own: on 10.9.0 a limited sort on an unindexed field is `LIMIT` over `SORT` over `COLLSCAN`, an equality on a unique single-field index is `FETCH` over `IXSCAN`, and an `$or` whose branches each have an index is a `COLLSCAN` | explains: the same limited sort is `SORT` over `COLLSCAN`, the limit folded into the sort; the equality is `EXPRESS_IXSCAN` on the unique index; the `$or` is `SUBPLAN` over `FETCH` over `OR` over one `IXSCAN` per branch |
165
166
  | aggregation-pipeline (array) updates | refused by the client with `invalid_argument` before any backend is contacted. A client-owned refusal, not an engine limit: both qualified engines apply a pipeline update themselves | same client refusal |
166
167
 
167
- `test/nosqldb/smartdb/test.engine-capability-boundary.node.ts` asserts the identity, grouped-totals, guarded-`$expr`, partial-index, collection-validator, write-conflict, `$elemMatch` and pipeline-update rows against both qualified engines, so a boundary that moves shows up as a failing suite rather than as a changed number in a consumer. The two halves of the grouped-totals, guarded-`$expr`, write-conflict and `$elemMatch` rows are separate tests, each bound to the engine that answers it, and the engine's own composite-key answer stays under assertion through the raw `aggregateGroupedTotals()` boundary — one literal row on 8.0.2, the two exact pairs on 10.7.2. The same boundary pins the rest of the engine's `$group` stage: a non-document accumulator is refused with `TypeMismatch` (14) on 10.7.2 while 8.0.2 answers it, and a `$cond` inside a composite key is evaluated per document on 10.7.2 while 8.0.2 answers its single literal row. It also pins whether a migration can create a partial index — 10.7.2 creates it and lists the filter, 8.0.2 refuses with 72 — and a collection validator — 10.7.2 creates and enforces it against model inserts and updates, 8.0.2 refuses with 72 — which writer a conflict sends back: on 10.7.2 the later transaction fails at its statement and re-runs while a write outside a transaction waits for the holder's commit, on 8.0.2 the later transaction commits first and the holder re-runs — and what `$elemMatch` answers through `getInstances()`: 10.7.2 matches the documented range and the `$not`/`$elemMatch`/`$nin` claim exactly as the MongoDB filter suite asserts them, 8.0.2 refuses both with 14, and a non-document operand is refused with 2 on 10.7.2 and 14 on 8.0.2. The `watch()` row is the contract refusal `ts/nosqldb/classes.collection.ts` states; the engine suite opens no change stream.
168
+ `test/nosqldb/smartdb/test.engine-capability-boundary.node.ts` asserts the identity, grouped-totals, guarded-`$expr`, partial-index, collection-validator, write-conflict, `$elemMatch` and pipeline-update rows against both qualified engines, so a boundary that moves shows up as a failing suite rather than as a changed number in a consumer. The two halves of the grouped-totals, guarded-`$expr`, write-conflict and `$elemMatch` rows are separate tests, each bound to the engine that answers it, and the engine's own composite-key answer stays under assertion through the raw `aggregateGroupedTotals()` boundary — one literal row on 8.0.2, the two exact pairs on 10.9.0. The same boundary pins the rest of the engine's `$group` stage: a non-document accumulator is refused with `TypeMismatch` (14) on 10.9.0 while 8.0.2 answers it, and a `$cond` inside a composite key is evaluated per document on 10.9.0 while 8.0.2 answers its single literal row. It also pins whether a migration can create a partial index — 10.9.0 creates it and lists the filter, 8.0.2 refuses with 72 — and a collection validator — 10.9.0 creates and enforces it against model inserts and updates, 8.0.2 refuses with 72 — which writer a conflict sends back: on 10.9.0 the later transaction fails at its statement and re-runs while a write outside a transaction waits for the holder's commit, on 8.0.2 the later transaction commits first and the holder re-runs — and what `$elemMatch` answers through `getInstances()`: 10.9.0 matches the documented range and the `$not`/`$elemMatch`/`$nin` claim exactly as the MongoDB filter suite asserts them, 8.0.2 refuses both with 14, and a non-document operand is refused with 2 on 10.9.0 and 14 on 8.0.2. The `watch()` row is the contract refusal `ts/nosqldb/classes.collection.ts` states; the engine suite opens no change stream.
168
169
 
169
170
  ## Relational SQL
170
171
 
@@ -244,14 +245,14 @@ Qualified engines:
244
245
 
245
246
  | Component | Qualified versions |
246
247
  | --- | --- |
247
- | NoSQLDB engine | **8.0.2 and 10.7.2** — the same `test/nosqldb/smartdb/` suite runs against both, through the single engine seam in `test/nosqldb/helpers/smartdb.ts`; `NOSQLDB_ENGINE=qualified` selects 10.7.2 |
248
+ | NoSQLDB engine | **8.0.2 and 10.9.0** — the same `test/nosqldb/smartdb/` suite runs against both, through the single engine seam in `test/nosqldb/helpers/smartdb.ts`; `NOSQLDB_ENGINE=qualified` selects 10.9.0 |
248
249
  | MongoDB | 8.0.26 replica sets |
249
250
  | ObjectStorage | 10.3.1 |
250
251
  | SQLDB | 0.2.3 |
251
252
  | MariaDB | 11.8 |
252
253
  | ClickHouse | 25.8 |
253
254
 
254
- Both engine versions run the whole document suite. They part on asserted boundaries — 10.7.2 groups composite keys exactly and answers two-field grouped totals, evaluates guarded `$expr` filters, creates migration-owned partial indexes and collection validators, reports a transaction write conflict at the statement and matches value-form `$elemMatch`, 8.0.2 does none of these, reports the conflict at commit, and the client refuses the two-field form there — which [Document APIs that need a MongoDB backend](#document-apis-that-need-a-mongodb-backend) states row by row. Text search, the Lucene adapter and the broad read-filter surface are exercised against MongoDB only. `pnpm run test:minio` uses a digest-pinned MinIO image: `sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e`. That MinIO version enforces the tested conditional uploads but does not enforce conditional deletion; its exact-purge capability is correctly unavailable. ObjectStorage passes both live probes. AWS S3 has not been qualified against a live account in this migration; no universal S3 exact-operation guarantee is inferred from the SDK.
255
+ Both engine versions run the whole document suite. They part on asserted boundaries — 10.9.0 groups composite keys exactly and answers two-field grouped totals, evaluates guarded `$expr` filters, creates migration-owned partial indexes and collection validators, reports a transaction write conflict at the statement and matches value-form `$elemMatch`, 8.0.2 does none of these, reports the conflict at commit, and the client refuses the two-field form there — which [Document APIs that need a MongoDB backend](#document-apis-that-need-a-mongodb-backend) states row by row. Text search, the Lucene adapter and the broad read-filter surface are exercised against MongoDB only. `pnpm run test:minio` uses a digest-pinned MinIO image: `sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e`. That MinIO version enforces the tested conditional uploads but does not enforce conditional deletion; its exact-purge capability is correctly unavailable. ObjectStorage passes both live probes. AWS S3 has not been qualified against a live account in this migration; no universal S3 exact-operation guarantee is inferred from the SDK.
255
256
 
256
257
  ## License and Legal Information
257
258
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@lossless.org/client',
6
- version: '1.22.2',
6
+ version: '1.23.1',
7
7
  description: 'One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.'
8
8
  }
@@ -10,7 +10,7 @@ import {
10
10
  reportSmartdataWrite, reportSmartdataWriteFailure, runWithOrdinarySmartdataSession,
11
11
  } from './classes.session.js';
12
12
  import { runSmartdataWrite } from './classes.readtracker.js';
13
- import { isSmartdataWriteRevisionsActive } from './classes.writerevisions.js';
13
+ import { requiresSmartdataWriteTransactions } from './classes.writelog.js';
14
14
  import {
15
15
  collectionModelSchemaResolverSymbol, collectionTopologyDbInspectionSymbol,
16
16
  getExpectedCollectionTopologyForSchema, inspectCollectionTopologySnapshot,
@@ -92,8 +92,8 @@ export const insertBootstrapRecord = async (
92
92
  if (topology.status !== 'absent' && topology.status !== 'exact') return 'invalid_configuration';
93
93
  const insertOptions = budget.operationOptions();
94
94
  dispatched = true;
95
- if (isSmartdataWriteRevisionsActive(dbArg)) {
96
- // The insert runs in a transaction of its own that commits its write revisions with it.
95
+ if (requiresSmartdataWriteTransactions(dbArg)) {
96
+ // The insert runs in a transaction of its own that commits its write revisions and write-log entry with it.
97
97
  try {
98
98
  const inserted = await runWithOrdinarySmartdataSession(undefined, dbArg, {
99
99
  ordinaryWrite: true, prepared: true, ...insertOptions,
@@ -1,4 +1,5 @@
1
1
  import { SmartdataDb } from './classes.db.js';
2
+ import type { ISmartdataWriteLogOptions } from './classes.writelog.js';
2
3
  import type { ISmartdataWriteRevisionsOptions } from './classes.writerevisions.js';
3
4
  import {
4
5
  composeSmartdataCapabilities,
@@ -17,6 +18,8 @@ export interface INoSqlConnectionOptions {
17
18
  connectionLimit?: number;
18
19
  /** See `ISmartdataDbOptions.writeRevisions`. */
19
20
  writeRevisions?: ISmartdataWriteRevisionsOptions;
21
+ /** See `ISmartdataDbOptions.writeLog`. */
22
+ writeLog?: ISmartdataWriteLogOptions;
20
23
  }
21
24
 
22
25
  export class NoSqlConnection extends SmartdataDb {
@@ -28,7 +31,8 @@ export class NoSqlConnection extends SmartdataDb {
28
31
  super({ mongoDbUrl: options.url, mongoDbName: options.database,
29
32
  mongoDbUser: options.username, mongoDbPass: options.password,
30
33
  ...(options.connectionLimit === undefined ? {} : { maxPoolSize: options.connectionLimit }),
31
- ...(options.writeRevisions === undefined ? {} : { writeRevisions: options.writeRevisions }) });
34
+ ...(options.writeRevisions === undefined ? {} : { writeRevisions: options.writeRevisions }),
35
+ ...(options.writeLog === undefined ? {} : { writeLog: options.writeLog }) });
32
36
  this.backend = options.backend;
33
37
  this.declaredCapabilities = composeSmartdataCapabilities(this.backend,
34
38
  smartdataCompositeGrouping(this.engineIdentity));
@@ -36,6 +36,16 @@ import {
36
36
  isSmartdataWriteRevisionsActive,
37
37
  type ISmartdataWriteRevisionsOptions,
38
38
  } from './classes.writerevisions.js';
39
+ import {
40
+ configureSmartdataWriteLog,
41
+ ensureSmartdataWriteLog,
42
+ followSmartdataWrites,
43
+ isSmartdataWriteLogActive,
44
+ stopSmartdataWriteFollowers,
45
+ type ISmartdataWriteFollower,
46
+ type ISmartdataWriteFollowerOptions,
47
+ type ISmartdataWriteLogOptions,
48
+ } from './classes.writelog.js';
39
49
  import {
40
50
  requireOperationSignal,
41
51
  requireOperationTimeoutMS,
@@ -50,6 +60,15 @@ export type {
50
60
  TSmartdataTransactionCommitOutcome,
51
61
  } from './classes.session.js';
52
62
  export type { ISmartdataWriteRevisionsOptions } from './classes.writerevisions.js';
63
+ export type {
64
+ ISmartdataCommitStamp,
65
+ ISmartdataFollowedWrite,
66
+ ISmartdataWriteFollower,
67
+ ISmartdataWriteFollowerOptions,
68
+ ISmartdataWriteGap,
69
+ ISmartdataWriteLogOptions,
70
+ TSmartdataWriteGapReason,
71
+ } from './classes.writelog.js';
53
72
  export type {
54
73
  ISmartdataWriteObserverOptions,
55
74
  ISmartdataWriteReport,
@@ -110,6 +129,13 @@ export interface ISmartdataDbOptions {
110
129
  * once, by the constructor.
111
130
  */
112
131
  writeRevisions?: ISmartdataWriteRevisionsOptions;
132
+ /**
133
+ * Keeps a write log, so `followWrites()` delivers the model writes of every
134
+ * process in commit order. Every process that writes the database must
135
+ * enable it with the fields its followers follow. Needs NoSQLDB 10.9.0 or
136
+ * newer; elsewhere it is inert. Read once, by the constructor.
137
+ */
138
+ writeLog?: ISmartdataWriteLogOptions;
113
139
  }
114
140
 
115
141
  export interface ISmartdataInvalidateTrackedReadsOptions {
@@ -246,6 +272,7 @@ export class SmartdataDb {
246
272
 
247
273
  constructor(smartdataOptions: ISmartdataDbOptions) {
248
274
  configureSmartdataWriteRevisions(this, smartdataOptions.writeRevisions);
275
+ configureSmartdataWriteLog(this, smartdataOptions.writeLog);
249
276
  this.smartdataOptions = smartdataOptions;
250
277
  this.status = 'initial';
251
278
  void this.statusConnectedDeferred.promise.catch(() => {});
@@ -342,6 +369,10 @@ export class SmartdataDb {
342
369
  await ensureSmartdataWriteRevisions(candidateDb, budget?.operationOptions());
343
370
  budget?.check();
344
371
  }
372
+ if (isSmartdataWriteLogActive(this)) {
373
+ await ensureSmartdataWriteLog(candidateDb, budget?.operationOptions());
374
+ budget?.check();
375
+ }
345
376
  this.mongoDbClient = candidateClient;
346
377
  this.mongoDb = candidateDb;
347
378
  candidateClient = undefined;
@@ -399,6 +430,14 @@ export class SmartdataDb {
399
430
  const mongoDbClient = this.pendingClient ?? this.mongoDbClient;
400
431
  try {
401
432
  const closeErrors: unknown[] = [];
433
+ try {
434
+ // Followers abort at once; their reads end with the client otherwise.
435
+ const backgroundStop = stopSmartdataWriteFollowers(this);
436
+ if (budget) await budget.waitFor(backgroundStop);
437
+ await backgroundStop;
438
+ } catch (errorArg) {
439
+ closeErrors.push(errorArg);
440
+ }
402
441
  try {
403
442
  const invalidation = invalidateSmartdataSessions(this, budget?.cleanupOptions());
404
443
  // A session whose cleanup outlives the deadline stays owned and invalidated
@@ -582,6 +621,31 @@ export class SmartdataDb {
582
621
  return addSmartdataWriteObserver(this, optionsArg);
583
622
  }
584
623
 
624
+ /**
625
+ * Follows the model writes of every process that writes this database with
626
+ * the `writeLog` option, from now on, in commit order: `onWrite` receives
627
+ * each committed transaction once, with the reports of its writes. A
628
+ * transaction of this instance is delivered as soon as its commit returned
629
+ * (`origin: 'local'`); every other one once the follower reads it from the
630
+ * engine's commit journal (`origin: 'journal'`, with its `stamp`), within
631
+ * 250 ms of a commit while the database is active and within a second when
632
+ * idle. A transaction of this instance whose commit outcome was unknown here
633
+ * is delivered from the journal if it committed. A commit the log cannot
634
+ * describe — a raw driver write, a migration, a writer without the option —
635
+ * names its collections in `unplaced`.
636
+ *
637
+ * The journal keeps a commit for 15 minutes, and at most its newest 65,536
638
+ * commits. A follower that falls further
639
+ * behind, or whose engine restarted on a new timeline, calls `onGap` and
640
+ * continues from the journal's head; state derived from earlier deliveries
641
+ * must then be rebuilt from the database. A failed journal read is logged
642
+ * and retried. Needs a connected database on NoSQLDB 10.9.0 or newer; MongoDB
643
+ * has no commit journal and is refused. `close()` stops every follower.
644
+ */
645
+ public async followWrites(optionsArg: ISmartdataWriteFollowerOptions): Promise<ISmartdataWriteFollower> {
646
+ return followSmartdataWrites(this, optionsArg);
647
+ }
648
+
585
649
  /**
586
650
  * Runs `fnArg` and records every model read it makes on this database:
587
651
  * `findOne`, `findAll`, `getCursor`, counts, pages and the exact reads.
@@ -30,7 +30,7 @@ import { SmartdataDbWatcher } from './classes.watcher.js';
30
30
  import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js';
31
31
  import { executeAtomicDelete, executeAtomicDeleteMany } from './classes.atomicdelete.js';
32
32
  import { requireOperationSignal } from './classes.operationdeadline.js';
33
- import { isSmartdataWriteRevisionsActive } from './classes.writerevisions.js';
33
+ import { requiresSmartdataWriteTransactions } from './classes.writelog.js';
34
34
  import {
35
35
  executeAtomicFindOneAndReplace, executeAtomicFindOneAndUpdate,
36
36
  } from './classes.atomicfindoneandupdate.js';
@@ -4677,9 +4677,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
4677
4677
  }
4678
4678
  const collection: SmartdataCollection<T> = (this as any).collection;
4679
4679
  const inTransaction = isOrdinarySessionInTransaction(opts?.session);
4680
- // Outside a transaction, a database with write revisions runs the batch
4681
- // in one of its own, so a failed batch inserted nothing.
4682
- const insertsWhole = !inTransaction && isSmartdataWriteRevisionsActive(collection.smartdataDb);
4680
+ // Outside a transaction, a database with write revisions or a write log
4681
+ // runs the batch in one of its own, so a failed batch inserted nothing.
4682
+ const insertsWhole = !inTransaction && requiresSmartdataWriteTransactions(collection.smartdataDb);
4683
4683
  const markInsertedDocuments = (
4684
4684
  insertedIdsArg: Record<number, unknown> | undefined,
4685
4685
  ): void => {
@@ -9,11 +9,11 @@ import { registerCollectionReconnectHandler } from './classes.collectionlifecycl
9
9
  import {
10
10
  SmartdataSession,
11
11
  acquireSmartdataSession,
12
- needsSmartdataRevisionTransaction,
12
+ needsSmartdataWriteTransaction,
13
13
  readPreviousWriteRevisions,
14
14
  reportSmartdataCollectionWrite,
15
15
  reportSmartdataWriteFailure,
16
- runSmartdataRevisionTransaction,
16
+ runSmartdataWriteTransaction,
17
17
  settleSmartdataOperationError,
18
18
  resolveSmartdataOperationDeadline,
19
19
  trackSmartdataRead,
@@ -1159,8 +1159,8 @@ export class SmartdataExactCollection<
1159
1159
  deadlineArg: SmartdataOperationDeadline,
1160
1160
  ) => Promise<TResult>,
1161
1161
  ): Promise<TResult> {
1162
- if (needsSmartdataRevisionTransaction(optionsArg.session, this.collection.smartdataDb)) {
1163
- return this.runRevisionWriteOperation(optionsArg, operationArg);
1162
+ if (needsSmartdataWriteTransaction(optionsArg.session, this.collection.smartdataDb)) {
1163
+ return this.runTransactionalWriteOperation(optionsArg, operationArg);
1164
1164
  }
1165
1165
  let started = false;
1166
1166
  try {
@@ -1180,13 +1180,14 @@ export class SmartdataExactCollection<
1180
1180
  }
1181
1181
 
1182
1182
  /**
1183
- * Runs one exact write in a transaction that carries the write's revisions:
1183
+ * Runs one exact write in a transaction that carries the write's revisions
1184
+ * and write-log entry:
1184
1185
  * on the caller's owned session, which needs the collection prepared, or on
1185
1186
  * a session of its own, which prepares the collection first. The
1186
1187
  * transaction's commit is the write's outcome: one whose commit outcome is
1187
1188
  * unknown is `ambiguous_write`.
1188
1189
  */
1189
- private async runRevisionWriteOperation<TResult>(
1190
+ private async runTransactionalWriteOperation<TResult>(
1190
1191
  optionsArg: IExactBoundedOptions,
1191
1192
  operationArg: (
1192
1193
  sessionArg: plugins.mongodb.ClientSession | undefined,
@@ -1198,7 +1199,7 @@ export class SmartdataExactCollection<
1198
1199
  signal?.throwIfAborted();
1199
1200
  if (optionsArg.session) this.requirePreparedForSession();
1200
1201
  try {
1201
- return await runSmartdataRevisionTransaction(optionsArg.session, this.collection.smartdataDb, {
1202
+ return await runSmartdataWriteTransaction(optionsArg.session, this.collection.smartdataDb, {
1202
1203
  ...(optionsArg.session ? { prepared: true } : { prepare: () => this.initialize() }),
1203
1204
  collectionName: this.collection.collectionName,
1204
1205
  ...(timeoutMS === undefined ? {} : { timeoutMS }),