@modelprofile.com/flexharness 11.0.0 → 11.1.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/classes.codesandbox.d.ts +12 -4
- package/dist_ts/classes.codesandbox.js +119 -287
- package/dist_ts/classes.codesandbox.worker.d.ts +52 -0
- package/dist_ts/classes.codesandbox.worker.js +310 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +37 -1
- package/dist_ts/classes.flexharness.callbacks.js +162 -53
- package/dist_ts/classes.flexharness.core.d.ts +2 -1
- package/dist_ts/classes.flexharness.core.js +22 -18
- package/dist_ts/classes.flexharness.results.js +4 -4
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -0
- package/dist_ts/classes.flexharness.reversions.js +40 -39
- package/dist_ts/classes.flexharness.runs.js +7 -3
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +2 -1
- package/dist_ts/classes.flexharness.tooldispatch.js +27 -9
- package/dist_ts/classes.stores.js +15 -13
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/index.d.ts +2 -1
- package/dist_ts/index.js +3 -2
- package/dist_ts/interfaces.d.ts +31 -9
- package/dist_ts/interfaces.flexharness.d.ts +14 -0
- package/dist_ts/interfaces.js +3 -3
- package/dist_ts/plugins.d.ts +6 -4
- package/dist_ts/plugins.js +10 -11
- package/dist_ts/utils.harnessoptions.d.ts +3 -1
- package/dist_ts/utils.harnessoptions.js +15 -2
- package/dist_ts/utils.json.d.ts +9 -1
- package/dist_ts/utils.json.js +92 -33
- package/dist_ts/utils.storecodecs.d.ts +1 -1
- package/dist_ts/utils.storecodecs.js +4 -4
- package/dist_ts/utils.storerules.d.ts +3 -3
- package/dist_ts/utils.storerules.js +1 -14
- package/dist_ts_mcp/index.d.ts +3 -1
- package/dist_ts_mcp/index.js +17 -2
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +18 -6
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +27 -16
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +33 -3
- package/dist_ts_stores_nosqldb/classes.recordstore.js +78 -18
- package/dist_ts_stores_nosqldb/index.d.ts +2 -2
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/package.json +4 -4
- package/readme.md +8 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +132 -276
- package/ts/classes.codesandbox.worker.ts +345 -0
- package/ts/classes.flexharness.callbacks.ts +207 -55
- package/ts/classes.flexharness.core.ts +22 -16
- package/ts/classes.flexharness.results.ts +3 -3
- package/ts/classes.flexharness.reversions.ts +43 -32
- package/ts/classes.flexharness.runs.ts +6 -2
- package/ts/classes.flexharness.tooldispatch.ts +23 -7
- package/ts/classes.stores.ts +21 -12
- package/ts/errors.ts +14 -0
- package/ts/index.ts +7 -1
- package/ts/interfaces.flexharness.ts +15 -0
- package/ts/interfaces.ts +32 -9
- package/ts/plugins.ts +9 -12
- package/ts/readme.md +8 -2
- package/ts/utils.harnessoptions.ts +22 -0
- package/ts/utils.json.ts +112 -35
- package/ts/utils.storecodecs.ts +3 -3
- package/ts/utils.storerules.ts +3 -24
- package/ts_mcp/index.ts +17 -1
- package/ts_mcp/readme.md +3 -1
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -15
- package/ts_stores_nosqldb/classes.recordstore.ts +105 -13
- package/ts_stores_nosqldb/index.ts +2 -0
- package/ts_stores_nosqldb/readme.md +10 -2
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import {
|
|
3
3
|
FlexHarnessStoreCommitUncertainError,
|
|
4
4
|
FlexHarnessStoreFormatError,
|
|
5
|
+
FlexHarnessStorePurgedError,
|
|
5
6
|
FlexHarnessValidationError,
|
|
6
7
|
} from '../ts/errors.js';
|
|
7
8
|
|
|
@@ -40,6 +41,7 @@ export interface IFlexRecordMutation<TValue> {
|
|
|
40
41
|
|
|
41
42
|
export const FLEX_RECORD_COLLECTION = 'flexharness_records';
|
|
42
43
|
export const FLEX_CHUNK_COLLECTION = 'flexharness_chunks';
|
|
44
|
+
export const FLEX_NAMESPACE_COLLECTION = 'flexharness_namespaces';
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* UTF-16 code units a record holds inline, and the size of each chunk of a longer payload. A
|
|
@@ -89,12 +91,31 @@ export class FlexChunkDocument extends plugins.SmartDataDbDoc<FlexChunkDocument,
|
|
|
89
91
|
public data!: string;
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
/**
|
|
95
|
+
* One storage namespace. Every record creation in the namespace increments `creationCount` in
|
|
96
|
+
* its transaction, so a concurrent fenced purge, which sets `fenced`, conflicts with it. A fenced
|
|
97
|
+
* namespace accepts no new record.
|
|
98
|
+
*/
|
|
99
|
+
export class FlexNamespaceDocument extends plugins.SmartDataDbDoc<FlexNamespaceDocument, FlexNamespaceDocument> {
|
|
100
|
+
public namespaceKey!: string;
|
|
101
|
+
public fenced!: boolean;
|
|
102
|
+
public creationCount!: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
type TRecordModel = typeof FlexRecordDocument;
|
|
93
106
|
type TChunkModel = typeof FlexChunkDocument;
|
|
107
|
+
type TNamespaceModel = typeof FlexNamespaceDocument;
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
interface IBoundModels {
|
|
110
|
+
records: TRecordModel;
|
|
111
|
+
chunks: TChunkModel;
|
|
112
|
+
namespaces: TNamespaceModel;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function bindModels(db: plugins.SmartdataDb): IBoundModels {
|
|
96
116
|
class BoundRecord extends FlexRecordDocument {}
|
|
97
117
|
class BoundChunk extends FlexChunkDocument {}
|
|
118
|
+
class BoundNamespace extends FlexNamespaceDocument {}
|
|
98
119
|
plugins.defineCollectionModel(BoundRecord, db, {
|
|
99
120
|
collectionName: FLEX_RECORD_COLLECTION,
|
|
100
121
|
persistedFields: [
|
|
@@ -142,7 +163,16 @@ function bindModels(db: plugins.SmartdataDb): { records: TRecordModel; chunks: T
|
|
|
142
163
|
{ name: 'flexharness_chunk_namespace', key: { namespaceKey: 1, domain: 1 } },
|
|
143
164
|
],
|
|
144
165
|
});
|
|
145
|
-
|
|
166
|
+
plugins.defineCollectionModel(BoundNamespace, db, {
|
|
167
|
+
collectionName: FLEX_NAMESPACE_COLLECTION,
|
|
168
|
+
persistedFields: ['namespaceKey', 'fenced', 'creationCount'],
|
|
169
|
+
numericFields: ['creationCount'],
|
|
170
|
+
identityFields: ['namespaceKey'],
|
|
171
|
+
indexes: [
|
|
172
|
+
{ name: 'flexharness_namespace_key', key: { namespaceKey: 1 }, options: { unique: true } },
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
return { records: BoundRecord, chunks: BoundChunk, namespaces: BoundNamespace };
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
interface IResolvedAddress {
|
|
@@ -208,13 +238,15 @@ export interface IFlexRecordStoreOptions {
|
|
|
208
238
|
* Compare-and-swap records on `@lossless.org/client/nosqldb`. A write reads the head, decides,
|
|
209
239
|
* writes any chunks, and swaps the head only while its `writeToken` is unchanged. Chunks of a
|
|
210
240
|
* replaced or abandoned payload are swept in a transaction that also touches the head, so a
|
|
211
|
-
* concurrent deletion or writer can never lose chunks its head references.
|
|
241
|
+
* concurrent deletion or writer can never lose chunks its head references. A head is created in
|
|
242
|
+
* a transaction that checks its namespace document, so a fenced namespace gains no record.
|
|
212
243
|
*/
|
|
213
244
|
export class FlexRecordStore {
|
|
214
245
|
private readonly db: plugins.SmartdataDb;
|
|
215
246
|
private readonly timeoutMs: number;
|
|
216
247
|
private readonly records: TRecordModel;
|
|
217
248
|
private readonly chunks: TChunkModel;
|
|
249
|
+
private readonly namespaces: TNamespaceModel;
|
|
218
250
|
private preparation?: Promise<void>;
|
|
219
251
|
|
|
220
252
|
constructor(options: IFlexRecordStoreOptions) {
|
|
@@ -224,10 +256,10 @@ export class FlexRecordStore {
|
|
|
224
256
|
}
|
|
225
257
|
this.db = options.db;
|
|
226
258
|
this.timeoutMs = timeoutMs;
|
|
227
|
-
({ records: this.records, chunks: this.chunks } = bindModels(options.db));
|
|
259
|
+
({ records: this.records, chunks: this.chunks, namespaces: this.namespaces } = bindModels(options.db));
|
|
228
260
|
}
|
|
229
261
|
|
|
230
|
-
/** Creates or verifies
|
|
262
|
+
/** Creates or verifies the collections and their indexes once; a failed attempt is retried. */
|
|
231
263
|
public prepare(): Promise<void> {
|
|
232
264
|
this.preparation ??= this.prepareCollections().catch((error: unknown) => {
|
|
233
265
|
this.preparation = undefined;
|
|
@@ -251,6 +283,7 @@ export class FlexRecordStore {
|
|
|
251
283
|
const options = { mode: 'ensureCompatible' as const, timeoutMs: this.timeoutMs };
|
|
252
284
|
await this.records.prepareCollection(options);
|
|
253
285
|
await this.chunks.prepareCollection(options);
|
|
286
|
+
await this.namespaces.prepareCollection(options);
|
|
254
287
|
}
|
|
255
288
|
|
|
256
289
|
private resolve(address: IFlexRecordAddress): IResolvedAddress {
|
|
@@ -318,26 +351,57 @@ export class FlexRecordStore {
|
|
|
318
351
|
);
|
|
319
352
|
}
|
|
320
353
|
|
|
321
|
-
/**
|
|
354
|
+
/**
|
|
355
|
+
* Deletes every record and chunk of a storage namespace in one domain, or in every domain
|
|
356
|
+
* together with its namespace document unless a fence holds it.
|
|
357
|
+
*/
|
|
322
358
|
public async purgeNamespace(storageKey: string, domain?: TFlexRecordDomain): Promise<void> {
|
|
323
359
|
await this.prepare();
|
|
324
360
|
const namespaceKey = digest(storageKey);
|
|
325
361
|
await this.deleteInTransaction(
|
|
326
362
|
`${FLEX_RECORD_COLLECTION}/${domain ?? 'all'}/namespace/${namespaceKey}`,
|
|
327
363
|
domain ? { namespaceKey, domain } : { namespaceKey },
|
|
364
|
+
domain
|
|
365
|
+
? undefined
|
|
366
|
+
: (transaction) => this.namespaces.atomicDeleteMany(
|
|
367
|
+
{ namespaceKey, fenced: false },
|
|
368
|
+
{ session: transaction },
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Fences a storage namespace and deletes every record and chunk of it in one transaction. The
|
|
375
|
+
* fence is permanent: every later record creation in the namespace is refused, and a creation
|
|
376
|
+
* that runs concurrently either commits first and is deleted, or conflicts and is refused.
|
|
377
|
+
*/
|
|
378
|
+
public async fenceNamespace(storageKey: string): Promise<void> {
|
|
379
|
+
await this.prepare();
|
|
380
|
+
const namespaceKey = digest(storageKey);
|
|
381
|
+
await this.deleteInTransaction(
|
|
382
|
+
`${FLEX_RECORD_COLLECTION}/all/namespace/${namespaceKey}`,
|
|
383
|
+
{ namespaceKey },
|
|
384
|
+
(transaction) => this.namespaces.atomicUpdate(
|
|
385
|
+
{ namespaceKey },
|
|
386
|
+
{ $set: { fenced: true }, $setOnInsert: { creationCount: 0 } },
|
|
387
|
+
{ upsert: true, session: transaction },
|
|
388
|
+
),
|
|
328
389
|
);
|
|
329
390
|
}
|
|
330
391
|
|
|
392
|
+
/** Deletes the selected records and chunks, after `namespace` changes the namespace document. */
|
|
331
393
|
private async deleteInTransaction(
|
|
332
394
|
path: string,
|
|
333
395
|
selector:
|
|
334
396
|
| { recordId: string; domain: TFlexRecordDomain }
|
|
335
397
|
| { ownerKey: string; domain: { $in: TFlexRecordDomain[] } }
|
|
336
398
|
| { namespaceKey: string; domain?: TFlexRecordDomain },
|
|
399
|
+
namespace?: (transaction: plugins.SmartdataSession) => Promise<unknown>,
|
|
337
400
|
): Promise<void> {
|
|
338
401
|
const session = this.db.createSession();
|
|
339
402
|
try {
|
|
340
403
|
await session.withTransaction(async (transaction) => {
|
|
404
|
+
await namespace?.(transaction);
|
|
341
405
|
await this.records.atomicDeleteMany(selector, { session: transaction });
|
|
342
406
|
await this.chunks.atomicDeleteMany(selector, { session: transaction });
|
|
343
407
|
}, { timeoutMS: this.timeoutMs });
|
|
@@ -480,11 +544,9 @@ export class FlexRecordStore {
|
|
|
480
544
|
head.payload = text;
|
|
481
545
|
head.chunked = false;
|
|
482
546
|
try {
|
|
483
|
-
await this.
|
|
484
|
-
return true;
|
|
547
|
+
return await this.create(resolved, head);
|
|
485
548
|
} catch (error) {
|
|
486
|
-
if (
|
|
487
|
-
if (isDefiniteRejection(error)) throw error;
|
|
549
|
+
if (plugins.SmartdataSession.getCommitOutcome(error) === 'not_committed') throw error;
|
|
488
550
|
throw new FlexHarnessStoreCommitUncertainError(resolved.path, 'write', { cause: error });
|
|
489
551
|
}
|
|
490
552
|
}
|
|
@@ -497,12 +559,42 @@ export class FlexRecordStore {
|
|
|
497
559
|
head.revision = 0;
|
|
498
560
|
head.payload = null;
|
|
499
561
|
head.chunked = true;
|
|
562
|
+
return await this.create(resolved, head) ? head : undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Inserts a new head unless a fence holds its namespace. The transaction also increments the
|
|
567
|
+
* namespace document, so it conflicts with a concurrent fenced purge: whichever commits second
|
|
568
|
+
* retries on a snapshot that holds the other. Returns false when another writer created the
|
|
569
|
+
* record first.
|
|
570
|
+
*/
|
|
571
|
+
private async create(resolved: IResolvedAddress, head: FlexRecordDocument): Promise<boolean> {
|
|
572
|
+
const session = this.db.createSession();
|
|
500
573
|
try {
|
|
501
|
-
await
|
|
502
|
-
|
|
574
|
+
await session.withTransaction(async (transaction) => {
|
|
575
|
+
const namespace = await this.namespaces.getInstance(
|
|
576
|
+
{ namespaceKey: resolved.namespaceKey },
|
|
577
|
+
{ session: transaction },
|
|
578
|
+
);
|
|
579
|
+
if (namespace?.fenced) throw new FlexHarnessStorePurgedError(resolved.path);
|
|
580
|
+
await this.namespaces.atomicUpdate(
|
|
581
|
+
{ namespaceKey: resolved.namespaceKey },
|
|
582
|
+
{ $inc: { creationCount: 1 }, $setOnInsert: { fenced: false } },
|
|
583
|
+
{ upsert: true, session: transaction },
|
|
584
|
+
);
|
|
585
|
+
await this.records.insert(head, { session: transaction });
|
|
586
|
+
}, { timeoutMS: this.timeoutMs });
|
|
587
|
+
return true;
|
|
503
588
|
} catch (error) {
|
|
504
|
-
if (
|
|
589
|
+
if (
|
|
590
|
+
isUniqueConflict(error) &&
|
|
591
|
+
plugins.SmartdataSession.getCommitOutcome(error) === 'not_committed'
|
|
592
|
+
) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
505
595
|
throw error;
|
|
596
|
+
} finally {
|
|
597
|
+
await session.close();
|
|
506
598
|
}
|
|
507
599
|
}
|
|
508
600
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export {
|
|
2
2
|
NoSqlFlexHarnessStores,
|
|
3
|
+
type INoSqlFlexHarnessPurgeOptions,
|
|
3
4
|
type INoSqlFlexHarnessStoresOptions,
|
|
4
5
|
} from './classes.nosqldbstores.js';
|
|
5
6
|
export {
|
|
6
7
|
FLEX_CHUNK_COLLECTION,
|
|
8
|
+
FLEX_NAMESPACE_COLLECTION,
|
|
7
9
|
FLEX_RECORD_CHUNK_CODE_UNITS,
|
|
8
10
|
FLEX_RECORD_COLLECTION,
|
|
9
11
|
} from './classes.recordstore.js';
|
|
@@ -19,17 +19,25 @@ const harness = new FlexHarness({ scopeResolver, modelResolver, stores });
|
|
|
19
19
|
|
|
20
20
|
`NoSqlFlexHarnessStores` implements every `IFlexHarnessStores` domain: scopes, projections with undo and redo history, remembered permissions, project management, Agent events with their archives, and background jobs. Several processes may open the same database; every save is a compare-and-swap on the stored record, so a stale writer gets the domain's conflict error instead of overwriting a newer snapshot.
|
|
21
21
|
|
|
22
|
-
The database is the application's. The stores never close it, and they need NoSQLDB or a MongoDB deployment with transactions. `prepare()` creates or verifies the `flexharness_records` and `
|
|
22
|
+
The database is the application's. The stores never close it, and they need NoSQLDB or a MongoDB deployment with transactions. `prepare()` creates or verifies the `flexharness_records`, `flexharness_chunks` and `flexharness_namespaces` collections and their indexes; every store operation awaits the same preparation, so calling it at startup only moves that work and its failure forward. `operationTimeoutMs` (1 to 120000, default 30000) bounds every database call and transaction.
|
|
23
23
|
|
|
24
24
|
A snapshot of up to `FLEX_RECORD_CHUNK_CODE_UNITS` UTF-16 code units of JSON is stored inside its record. A longer one is split into chunks of that size, each below 3 MiB of UTF-8, so a snapshot may exceed the 16 MiB document limit of NoSQLDB and MongoDB. Chunks are written before the record swaps to them. The chunks a record no longer references are removed by a later write of that record, in a transaction that also touches the record, and deleting a session or purging a namespace removes its records and chunks in one transaction. A writer that crashes between writing its chunks and swapping the record leaves only unreferenced chunks, never a record that points at missing ones. They are written against the record's current write token, which a concurrent writer may still win with, so the write after the next one removes them, or the deletion of the session or namespace does.
|
|
25
25
|
|
|
26
|
-
`stores.purgeNamespace(storageKey)` is the durable deletion of a scope: it removes every record and chunk of that storage namespace in every domain, including the scope snapshot and the project-management tombstones that deleting its sessions keeps, in one transaction. Deleting every session alone leaves those records, keyed by the storage key.
|
|
26
|
+
`stores.purgeNamespace(storageKey)` is the durable deletion of a scope: it removes every record and chunk of that storage namespace in every domain, including the scope snapshot and the project-management tombstones that deleting its sessions keeps, in one transaction. Deleting every session alone leaves those records, keyed by the storage key. Stop new admission for every alias of the scope first. A plain purge must then wait until `harness.retireScope()` has returned on every harness that owns the scope: a harness that still holds it can write records back, because the purge removed the revisions its writes would conflict with.
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
29
|
await harness.retireScope(scopeId);
|
|
30
30
|
await stores.purgeNamespace(storageKey);
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
When a harness may still hold the scope, such as one on another server, purge with `{ fence: true }`. The same transaction fences the namespace in `flexharness_namespaces`: every later record creation in it, from any process, is refused with `FlexHarnessStorePurgedError`, and a write that extends a purged record conflicts, so nothing a stale harness writes lands. A creation that races the purge either commits first and is removed with the rest, or is refused. The fence is permanent, so the storage key is never usable again, and a later purge without the fence does not lift it. FlexHarness reports a refused write of a session operation through `externalErrorProjector`, like every other store failure there.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
await stores.purgeNamespace(storageKey, { fence: true });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Every record creation is a transaction that checks the namespace's fence document and writes to it, so concurrent creations in one namespace contend on that document and the driver retries the ones that conflict.
|
|
40
|
+
|
|
33
41
|
A record swap that loses to a concurrent writer is decided again against the newer record, up to eight times, with a bounded exponential backoff between attempts; a writer that still loses gets the domain's conflict error.
|
|
34
42
|
|
|
35
43
|
A save that may have reached the database without a confirmation, such as a timed-out or interrupted record write or a deletion transaction with an unknown commit outcome, rejects with `FlexHarnessStoreCommitUncertainError` (operation `write` or `delete`). FlexHarness then fences the namespace, as it does for the JSON file stores.
|