@lossless.org/client 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/nosqldb/classes.db.d.ts +20 -3
- package/dist_ts/nosqldb/classes.db.js +45 -12
- package/dist_ts/nosqldb/classes.doc.d.ts +12 -2
- package/dist_ts/nosqldb/classes.doc.js +1 -1
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +2 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +7 -1
- package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.persistence.js +8 -1
- package/dist_ts/nosqldb/classes.session.d.ts +40 -1
- package/dist_ts/nosqldb/classes.session.js +188 -32
- package/package.json +4 -4
- package/readme.md +11 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.db.ts +68 -12
- package/ts/nosqldb/classes.doc.ts +19 -2
- package/ts/nosqldb/classes.operationbudget.ts +6 -0
- package/ts/nosqldb/classes.persistence.ts +9 -0
- package/ts/nosqldb/classes.session.ts +256 -41
package/readme.md
CHANGED
|
@@ -104,6 +104,9 @@ Family imports preserve the established SmartData and SmartBucket names and cons
|
|
|
104
104
|
| Key-value store readiness | `EasyStore.ensureInitialized()` — installs the store's declared index at startup, outside any transaction window, so a first `readKey()`/`writeKey()` needs no index DDL |
|
|
105
105
|
| Key-value store copies | `EasyStore` `readKey()`/`readAll()` return copies and `writeKey()`/`writeAll()`/`replace()` store copies, in the persisted BSON form, so a read result is a stable snapshot and a caller-side mutation never reaches the store |
|
|
106
106
|
| Client deadlines | `timeoutMS` (1..120000) on every ordinary model read and write, instance `save()`/`saveIf()`/`delete()` included, on every exact call, and `session.withTransaction(callback, { timeoutMS })` for a whole transaction, retries and commit included; expiry rejects with `SmartdataPersistenceError` code `timeout`, and an exact write interrupted in flight with `ambiguous_write` |
|
|
107
|
+
| Bounded database close | `db.close({ timeoutMs })` bounds the whole close — session cleanup, the optional database removal and the driver client's close — and rejects at its deadline with `SmartdataPersistenceError` code `timeout`, leaving the database `close_failed`; the driver's close keeps running until it settles on its own, and the next `close()` joins it |
|
|
108
|
+
| Transactions ended by close | a `session.withTransaction()` whose session `db.close()` ends while it runs rejects with `SmartdataPersistenceError` code `unsupported_operation` — "nothing was committed" before the commit, "the commit outcome is unknown" during it — instead of resolving with the callback's result |
|
|
109
|
+
| Transaction commit outcome | `SmartdataSession.getCommitOutcome(error)` states for every value a `session.withTransaction()` rejected with whether the transaction could have committed: `not_committed` (nothing was committed) or `unknown` (a commit was sent and never confirmed); the rejection itself keeps its identity |
|
|
107
110
|
| Cancellation | `signal` on every exact call, `atomicDeleteMany()` and `session.withTransaction(callback, { signal })`, which interrupts the session's operation in flight, rolls the attempt back and never commits after an abort |
|
|
108
111
|
| 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 |
|
|
109
112
|
|
|
@@ -120,7 +123,7 @@ A bucket lookup sends one HeadBucket for the named bucket, never ListBuckets, so
|
|
|
120
123
|
|
|
121
124
|
### Document APIs that need a MongoDB backend
|
|
122
125
|
|
|
123
|
-
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.
|
|
126
|
+
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.
|
|
124
127
|
|
|
125
128
|
`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.
|
|
126
129
|
|
|
@@ -130,12 +133,13 @@ The document model runs on both NoSQLDB and MongoDB, but several APIs do not mea
|
|
|
130
133
|
| the same with **two** `groupBy` fields | exact, `sums` 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 |
|
|
131
134
|
| 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 |
|
|
132
135
|
| `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 | supported on replica sets |
|
|
133
|
-
| 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.
|
|
134
|
-
| collection validators (`validator`, `validationLevel`, `validationAction`) | no client API; a migration owns a validator through the driver. 10.7.0
|
|
135
|
-
| write conflicts between transactions, and writes outside a transaction to a document a transaction holds | 10.7.
|
|
136
|
+
| 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 |
|
|
137
|
+
| 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 |
|
|
138
|
+
| 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 |
|
|
139
|
+
| 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 |
|
|
136
140
|
| 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 |
|
|
137
141
|
|
|
138
|
-
`test/nosqldb/smartdb/test.engine-capability-boundary.node.ts` asserts the identity, grouped-totals, guarded-`$expr`, partial-index, collection-validator, write-conflict 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
|
|
142
|
+
`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.
|
|
139
143
|
|
|
140
144
|
## Relational SQL
|
|
141
145
|
|
|
@@ -215,14 +219,14 @@ Qualified engines:
|
|
|
215
219
|
|
|
216
220
|
| Component | Qualified versions |
|
|
217
221
|
| --- | --- |
|
|
218
|
-
| NoSQLDB engine | **8.0.2 and 10.7.
|
|
222
|
+
| 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 |
|
|
219
223
|
| MongoDB | 8.0.26 replica sets |
|
|
220
224
|
| ObjectStorage | 10.3.1 |
|
|
221
225
|
| SQLDB | 0.2.3 |
|
|
222
226
|
| MariaDB | 11.8 |
|
|
223
227
|
| ClickHouse | 25.8 |
|
|
224
228
|
|
|
225
|
-
Both engine versions run the whole document suite. They part on asserted boundaries — 10.7.
|
|
229
|
+
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.
|
|
226
230
|
|
|
227
231
|
## License and Legal Information
|
|
228
232
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/nosqldb/classes.db.ts
CHANGED
|
@@ -2,7 +2,11 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
|
|
3
3
|
import { SmartdataCollection } from './classes.collection.js';
|
|
4
4
|
import { EasyStore } from './classes.easystore.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createSmartdataTimeoutError,
|
|
7
|
+
isMongoDeadlineError,
|
|
8
|
+
SmartdataPersistenceError,
|
|
9
|
+
} from './classes.persistence.js';
|
|
6
10
|
import { SmartdataMongoAdministration } from './classes.mongoadministration.js';
|
|
7
11
|
import {
|
|
8
12
|
createSmartdataSession,
|
|
@@ -18,7 +22,11 @@ import {
|
|
|
18
22
|
} from './classes.namespaceinspection.js';
|
|
19
23
|
|
|
20
24
|
export { SmartdataSession } from './classes.session.js';
|
|
21
|
-
export type {
|
|
25
|
+
export type {
|
|
26
|
+
ISmartdataTransactionOptions,
|
|
27
|
+
TSmartdataOrdinarySession,
|
|
28
|
+
TSmartdataTransactionCommitOutcome,
|
|
29
|
+
} from './classes.session.js';
|
|
22
30
|
|
|
23
31
|
import { logger } from './logging.js';
|
|
24
32
|
import { assertDataOptions, SmartdataOperationBudget } from './classes.operationbudget.js';
|
|
@@ -78,7 +86,14 @@ export interface ISmartdataInitOptions {
|
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
export interface ISmartdataCloseOptions {
|
|
81
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Deadline for the whole close (1..120000 ms): session cleanup, the optional
|
|
91
|
+
* database removal and the driver client's close. When it expires the call
|
|
92
|
+
* rejects with `SmartdataPersistenceError` code `timeout` and the database is
|
|
93
|
+
* `close_failed`. Cleanup still running at that point is not abandoned: it
|
|
94
|
+
* stays owned by this `SmartdataDb`, and the next `close()` joins it.
|
|
95
|
+
* Without `timeoutMs`, close waits for every cleanup to settle.
|
|
96
|
+
*/
|
|
82
97
|
timeoutMs?: number;
|
|
83
98
|
dropDatabase?: {
|
|
84
99
|
expectedDatabaseName: string;
|
|
@@ -120,6 +135,9 @@ type TSmartdataDbLifecycleState =
|
|
|
120
135
|
|
|
121
136
|
type TSmartdataDbLifecycleOperation = 'init' | 'close';
|
|
122
137
|
|
|
138
|
+
/** How the driver's close of an owned client ended, kept until a close() has observed it. */
|
|
139
|
+
type TSmartdataClientCloseOutcome = { failed: false } | { failed: true; error: unknown };
|
|
140
|
+
|
|
123
141
|
const readinessDefaultTimeoutMs = 2_000;
|
|
124
142
|
const readinessMaximumTimeoutMs = 30_000;
|
|
125
143
|
|
|
@@ -177,6 +195,8 @@ export class SmartdataDb {
|
|
|
177
195
|
private lifecycleEpoch = 0;
|
|
178
196
|
private activeLifecycleOperation: TSmartdataDbLifecycleOperation | null = null;
|
|
179
197
|
private pendingClient?: plugins.mongodb.MongoClient;
|
|
198
|
+
/** The driver's close of the owned client until a close() has observed its outcome. */
|
|
199
|
+
private clientClosing?: Promise<TSmartdataClientCloseOutcome>;
|
|
180
200
|
private engineIdentityState: ISmartdataEngineIdentity = unidentifiedSmartdataEngine;
|
|
181
201
|
private capabilitiesState: ICapabilities | undefined;
|
|
182
202
|
|
|
@@ -327,7 +347,8 @@ export class SmartdataDb {
|
|
|
327
347
|
}
|
|
328
348
|
|
|
329
349
|
/**
|
|
330
|
-
*
|
|
350
|
+
* Closes the connection to the database. `timeoutMs` bounds the whole call;
|
|
351
|
+
* see `ISmartdataCloseOptions`.
|
|
331
352
|
*/
|
|
332
353
|
public async close(optionsArg: ISmartdataCloseOptions = {}): Promise<void> {
|
|
333
354
|
this.assertLifecycleOperationCanBegin('close');
|
|
@@ -341,7 +362,14 @@ export class SmartdataDb {
|
|
|
341
362
|
try {
|
|
342
363
|
const closeErrors: unknown[] = [];
|
|
343
364
|
try {
|
|
344
|
-
|
|
365
|
+
const invalidation = invalidateSmartdataSessions(this, budget?.cleanupOptions());
|
|
366
|
+
// A session whose cleanup outlives the deadline stays owned and invalidated
|
|
367
|
+
// until the next close() ends it. Its abort already carries the same
|
|
368
|
+
// deadline, so a late failure can only be that deadline, which this close
|
|
369
|
+
// reports as `timeout`.
|
|
370
|
+
invalidation.catch(() => {});
|
|
371
|
+
if (budget) await budget.waitFor(invalidation);
|
|
372
|
+
await invalidation;
|
|
345
373
|
} catch (errorArg) {
|
|
346
374
|
closeErrors.push(
|
|
347
375
|
...(unwrapSmartdataSessionInvalidationErrors(errorArg) ?? [errorArg]),
|
|
@@ -360,16 +388,28 @@ export class SmartdataDb {
|
|
|
360
388
|
closeErrors.push(errorArg);
|
|
361
389
|
}
|
|
362
390
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
391
|
+
if (mongoDbClient) {
|
|
392
|
+
const clientClosing = this.closeOwnedClient(mongoDbClient);
|
|
393
|
+
try {
|
|
394
|
+
if (budget) await budget.waitFor(clientClosing);
|
|
395
|
+
// Settled in time: this close observes the outcome, and a later one starts afresh.
|
|
396
|
+
const outcome = await clientClosing;
|
|
397
|
+
this.clientClosing = undefined;
|
|
398
|
+
if (outcome.failed) throw outcome.error;
|
|
399
|
+
this.pendingClient = undefined;
|
|
400
|
+
} catch (errorArg) {
|
|
401
|
+
closeErrors.push(errorArg);
|
|
402
|
+
}
|
|
368
403
|
}
|
|
369
404
|
if (closeErrors.length > 0) {
|
|
370
405
|
this.transitionLifecycle('close_failed');
|
|
371
|
-
if (budget)
|
|
372
|
-
|
|
406
|
+
if (budget) {
|
|
407
|
+
throw budget.expired() || closeErrors.some(isMongoDeadlineError)
|
|
408
|
+
? createSmartdataTimeoutError(
|
|
409
|
+
'SmartData database close exceeded its deadline; close must be retried.')
|
|
410
|
+
: new SmartdataPersistenceError('unsupported_operation',
|
|
411
|
+
'SmartData database cleanup failed; close must be retried.');
|
|
412
|
+
}
|
|
373
413
|
if (closeErrors.length === 1) {
|
|
374
414
|
throw closeErrors[0];
|
|
375
415
|
}
|
|
@@ -653,6 +693,22 @@ export class SmartdataDb {
|
|
|
653
693
|
return this.mongoDb;
|
|
654
694
|
}
|
|
655
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Starts the driver's close of an owned client, or joins the one a previous
|
|
698
|
+
* close() started. The driver offers no deadline for it, so a close that
|
|
699
|
+
* outlived its caller keeps running, and its outcome — a rejection included —
|
|
700
|
+
* waits here for the next close() instead of being lost.
|
|
701
|
+
*/
|
|
702
|
+
private closeOwnedClient(
|
|
703
|
+
clientArg: plugins.mongodb.MongoClient,
|
|
704
|
+
): Promise<TSmartdataClientCloseOutcome> {
|
|
705
|
+
this.clientClosing ??= clientArg.close().then(
|
|
706
|
+
(): TSmartdataClientCloseOutcome => ({ failed: false }),
|
|
707
|
+
(errorArg: unknown): TSmartdataClientCloseOutcome => ({ failed: true, error: errorArg }),
|
|
708
|
+
);
|
|
709
|
+
return this.clientClosing;
|
|
710
|
+
}
|
|
711
|
+
|
|
656
712
|
private endLifecycleOperation(operationArg: TSmartdataDbLifecycleOperation): void {
|
|
657
713
|
if (this.activeLifecycleOperation === operationArg) {
|
|
658
714
|
this.activeLifecycleOperation = null;
|
|
@@ -561,7 +561,24 @@ export type TSmartdataOptionalNullPredicate<TValue> =
|
|
|
561
561
|
type InValues<T> = ReadonlyArray<ElementOf<T> | TSmartdataOptionalNullPredicate<T>>;
|
|
562
562
|
|
|
563
563
|
// Type that allows MongoDB operators on leaf values while maintaining nested type safety
|
|
564
|
-
export type MongoFilterCondition<T> =
|
|
564
|
+
export type MongoFilterCondition<T> =
|
|
565
|
+
| T
|
|
566
|
+
| TSmartdataOptionalNullPredicate<T>
|
|
567
|
+
| TSmartdataFilterOperators<T>;
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The operand of `$elemMatch`: a query on each element, the document form, or
|
|
571
|
+
* an operator document applied to each element as a whole, the value form
|
|
572
|
+
* (`{ $gte: 80, $lt: 90 }`). Elements that are all documents take only the
|
|
573
|
+
* document form; any other element type takes either, so element unions that
|
|
574
|
+
* include documents, nullable documents and `unknown` keep the document form.
|
|
575
|
+
*/
|
|
576
|
+
type TSmartdataElemMatchOperand<TElement> = [TElement] extends [object]
|
|
577
|
+
? MongoFilter<TElement>
|
|
578
|
+
: MongoFilter<TElement> | TSmartdataFilterOperators<TElement>;
|
|
579
|
+
|
|
580
|
+
/** The operator document of a field condition. */
|
|
581
|
+
type TSmartdataFilterOperators<T> = {
|
|
565
582
|
$eq?: T | TSmartdataOptionalNullPredicate<T>;
|
|
566
583
|
$ne?: T | TSmartdataOptionalNullPredicate<T>;
|
|
567
584
|
$gt?: T;
|
|
@@ -575,7 +592,7 @@ export type MongoFilterCondition<T> = T | TSmartdataOptionalNullPredicate<T> | {
|
|
|
575
592
|
$regex?: string | RegExp;
|
|
576
593
|
$options?: string;
|
|
577
594
|
$all?: T extends ReadonlyArray<infer U> ? ReadonlyArray<U> : never;
|
|
578
|
-
$elemMatch?: T extends ReadonlyArray<infer U> ?
|
|
595
|
+
$elemMatch?: T extends ReadonlyArray<infer U> ? TSmartdataElemMatchOperand<U> : never;
|
|
579
596
|
$size?: T extends ReadonlyArray<any> ? number : never;
|
|
580
597
|
$not?: MongoFilterCondition<T>;
|
|
581
598
|
};
|
|
@@ -46,6 +46,12 @@ export class SmartdataOperationBudget {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Whether the deadline itself has passed; a caller's signal does not count. */
|
|
50
|
+
public expired(): boolean {
|
|
51
|
+
if (performance.now() >= this.expiresAt) this.controller.abort();
|
|
52
|
+
return this.controller.signal.aborted;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
public remainingMs(): number {
|
|
50
56
|
this.check();
|
|
51
57
|
return this.cleanupOptions().timeoutMS;
|
|
@@ -69,6 +69,15 @@ export const isMongoTransactionRetryError = (errorArg: unknown): boolean =>
|
|
|
69
69
|
errorArg.hasErrorLabel('UnknownTransactionCommitResult')
|
|
70
70
|
);
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The server or the driver stated that the transaction was aborted and may be
|
|
74
|
+
* run again from its start, so nothing of it was committed. A timeout the
|
|
75
|
+
* driver raises in place of such an error copies its labels.
|
|
76
|
+
*/
|
|
77
|
+
export const isMongoTransientTransactionError = (errorArg: unknown): boolean =>
|
|
78
|
+
errorArg instanceof plugins.mongodb.MongoError
|
|
79
|
+
&& errorArg.hasErrorLabel(plugins.mongodb.MongoErrorLabel.TransientTransactionError);
|
|
80
|
+
|
|
72
81
|
export const isMongoDuplicateKeyError = (
|
|
73
82
|
errorArg: unknown,
|
|
74
83
|
): errorArg is plugins.mongodb.MongoServerError => {
|