@modelprofile.com/flexharness 11.3.0 → 12.0.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.flexharness.core.d.ts +4 -0
- package/dist_ts/classes.flexharness.core.js +69 -25
- package/dist_ts/classes.flexharness.imports.js +4 -3
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -1
- package/dist_ts/classes.flexharness.reversions.js +43 -29
- package/dist_ts/classes.flexharness.sessions.js +2 -6
- package/dist_ts/classes.stores.js +20 -18
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +24 -2
- package/dist_ts/interfaces.flexharness.d.ts +8 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.projectionchange.d.ts +34 -0
- package/dist_ts/utils.projectionchange.js +135 -0
- package/dist_ts/utils.storecodecs.d.ts +10 -3
- package/dist_ts/utils.storecodecs.js +29 -4
- package/dist_ts/utils.storerules.d.ts +1 -1
- package/dist_ts/utils.storerules.js +1 -1
- package/dist_ts_migration/v3_legacyflexharness.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +5 -4
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +56 -54
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +176 -19
- package/dist_ts_stores_nosqldb/classes.recordstore.js +392 -67
- package/dist_ts_stores_nosqldb/index.d.ts +1 -1
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/dist_ts_stores_nosqldb/plugins.d.ts +1 -1
- package/dist_ts_stores_nosqldb/plugins.js +1 -1
- package/dist_ts_stores_testing/contract.d.ts +33 -0
- package/dist_ts_stores_testing/contract.js +566 -0
- package/dist_ts_stores_testing/fixtures.d.ts +20 -0
- package/dist_ts_stores_testing/fixtures.js +112 -0
- package/dist_ts_stores_testing/index.d.ts +2 -0
- package/dist_ts_stores_testing/index.js +3 -0
- package/dist_ts_stores_testing/plugins.d.ts +5 -0
- package/dist_ts_stores_testing/plugins.js +7 -0
- package/package.json +9 -3
- package/readme.md +6 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.core.ts +74 -24
- package/ts/classes.flexharness.imports.ts +3 -2
- package/ts/classes.flexharness.reversions.ts +53 -28
- package/ts/classes.flexharness.sessions.ts +2 -5
- package/ts/classes.stores.ts +40 -37
- package/ts/index.ts +5 -0
- package/ts/interfaces.flexharness.ts +8 -1
- package/ts/interfaces.ts +27 -2
- package/ts/plugins.ts +12 -7
- package/ts/readme.md +6 -3
- package/ts/utils.projectionchange.ts +190 -0
- package/ts/utils.storecodecs.ts +42 -7
- package/ts/utils.storerules.ts +2 -2
- package/ts_migration/v3_legacyflexharness.ts +11 -6
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +79 -71
- package/ts_stores_nosqldb/classes.recordstore.ts +491 -66
- package/ts_stores_nosqldb/index.ts +1 -0
- package/ts_stores_nosqldb/plugins.ts +4 -3
- package/ts_stores_nosqldb/readme.md +6 -2
- package/ts_stores_testing/contract.ts +776 -0
- package/ts_stores_testing/fixtures.ts +127 -0
- package/ts_stores_testing/index.ts +17 -0
- package/ts_stores_testing/plugins.ts +20 -0
- package/ts_stores_testing/readme.md +62 -0
package/ts/utils.storerules.ts
CHANGED
|
@@ -144,8 +144,8 @@ export function isProjectManagementTombstoneAdmitted(
|
|
|
144
144
|
|
|
145
145
|
/** An archive is written once; repeating it with the same events is a no-op. */
|
|
146
146
|
export function assertSameArchiveEvents(
|
|
147
|
-
existing: plugins.
|
|
148
|
-
next: plugins.
|
|
147
|
+
existing: plugins.IAgentEventArchive,
|
|
148
|
+
next: plugins.IAgentEventArchive,
|
|
149
149
|
): void {
|
|
150
150
|
if (
|
|
151
151
|
JSON.stringify(existing.events.map((event) => event.id))
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
assertJsonSerializable,
|
|
19
19
|
cloneSerializable,
|
|
20
20
|
} from '../ts/utils.json.js';
|
|
21
|
+
import { createWholeProjectionChange } from '../ts/utils.projectionchange.js';
|
|
21
22
|
|
|
22
23
|
export interface IFlexLegacyStoredSessionSnapshot {
|
|
23
24
|
session: IFlexSession;
|
|
@@ -59,8 +60,8 @@ interface IInspectedSessionDestination {
|
|
|
59
60
|
plan: ILegacySessionMigrationPlan;
|
|
60
61
|
projection: TFlexProjectionSnapshot | undefined;
|
|
61
62
|
permission: IFlexPermissionSnapshot | undefined;
|
|
62
|
-
eventStore: plugins.
|
|
63
|
-
eventSnapshot: plugins.
|
|
63
|
+
eventStore: plugins.IAgentEventStore;
|
|
64
|
+
eventSnapshot: plugins.IAgentEventSnapshot | undefined;
|
|
64
65
|
jobSnapshot: plugins.IToolJobSnapshot | undefined;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -341,7 +342,7 @@ function createSessionAgentEvents(
|
|
|
341
342
|
|
|
342
343
|
const jsonEvents = JSON.parse(JSON.stringify(events)) as plugins.TAgentEvent[];
|
|
343
344
|
try {
|
|
344
|
-
plugins.
|
|
345
|
+
plugins.validateAgentEventSnapshot({
|
|
345
346
|
schemaVersion: 2,
|
|
346
347
|
sessionId,
|
|
347
348
|
revision: 1,
|
|
@@ -508,7 +509,7 @@ async function inspectDestinations(
|
|
|
508
509
|
projection,
|
|
509
510
|
permission,
|
|
510
511
|
eventStore,
|
|
511
|
-
eventSnapshot
|
|
512
|
+
eventSnapshot,
|
|
512
513
|
jobSnapshot,
|
|
513
514
|
});
|
|
514
515
|
}
|
|
@@ -596,7 +597,7 @@ export async function migrateLegacyFlexHarnessSnapshot(
|
|
|
596
597
|
await stores.projections.save(
|
|
597
598
|
storageKey,
|
|
598
599
|
session.sessionId,
|
|
599
|
-
session.projectionSnapshot,
|
|
600
|
+
createWholeProjectionChange(session.projectionSnapshot),
|
|
600
601
|
0,
|
|
601
602
|
);
|
|
602
603
|
}
|
|
@@ -609,7 +610,11 @@ export async function migrateLegacyFlexHarnessSnapshot(
|
|
|
609
610
|
);
|
|
610
611
|
}
|
|
611
612
|
if (!destination.eventSnapshot) {
|
|
612
|
-
await destination.eventStore.save(
|
|
613
|
+
await destination.eventStore.save(
|
|
614
|
+
session.sessionId,
|
|
615
|
+
{ remove: [], replace: [], append: session.agentEvents },
|
|
616
|
+
0,
|
|
617
|
+
);
|
|
613
618
|
}
|
|
614
619
|
}
|
|
615
620
|
if (!destinations.scope) {
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
IFlexProjectManagementStore,
|
|
10
10
|
IFlexProjectManagementTombstone,
|
|
11
11
|
IFlexProjectManagementWriteContext,
|
|
12
|
-
|
|
12
|
+
IFlexProjectionChange,
|
|
13
13
|
IFlexProjectionStore,
|
|
14
14
|
IFlexResultStore,
|
|
15
15
|
IFlexScopeSnapshot,
|
|
@@ -30,10 +30,16 @@ import {
|
|
|
30
30
|
assertFlexProjectManagementSnapshot,
|
|
31
31
|
assertFlexProjectManagementTombstone,
|
|
32
32
|
} from '../ts/utils.projectmanagement.js';
|
|
33
|
+
import {
|
|
34
|
+
applyFlexProjectionChange,
|
|
35
|
+
assertFlexProjectionChange,
|
|
36
|
+
validateProjectionChangeSave,
|
|
37
|
+
} from '../ts/utils.projectionchange.js';
|
|
33
38
|
import {
|
|
34
39
|
assertFlexToolJobSnapshot,
|
|
35
|
-
|
|
40
|
+
createChangedAgentEventSnapshot,
|
|
36
41
|
createToolJobSnapshot,
|
|
42
|
+
normalizeAgentEventChange,
|
|
37
43
|
normalizeJsonObjectUndefined,
|
|
38
44
|
validateAgentEventArchive,
|
|
39
45
|
validateAgentEventSnapshot,
|
|
@@ -49,17 +55,27 @@ import {
|
|
|
49
55
|
validateResultIndex,
|
|
50
56
|
validateSnapshotSave,
|
|
51
57
|
} from '../ts/utils.storerules.js';
|
|
52
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
FlexRecordStore,
|
|
60
|
+
type IFlexRecordLog,
|
|
61
|
+
type IFlexRecordReplacement,
|
|
62
|
+
} from './classes.recordstore.js';
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
/** Replaces a record with a snapshot while the record's revision is the expected one. */
|
|
65
|
+
function revisionReplacement<TSnapshot extends { revision: number }>(
|
|
56
66
|
snapshot: TSnapshot,
|
|
57
67
|
expectedRevision: number,
|
|
58
68
|
conflict: (actualRevision: number) => Error,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
abortSignal?: AbortSignal,
|
|
70
|
+
): IFlexRecordReplacement {
|
|
71
|
+
return {
|
|
72
|
+
decide: (actualRevision) => {
|
|
73
|
+
abortSignal?.throwIfAborted();
|
|
74
|
+
if (actualRevision !== expectedRevision) throw conflict(actualRevision);
|
|
75
|
+
return { revision: snapshot.revision, value: snapshot };
|
|
76
|
+
},
|
|
77
|
+
contention: conflict,
|
|
78
|
+
};
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
function validateScope(value: unknown): IFlexScopeSnapshot {
|
|
@@ -99,50 +115,49 @@ class NoSqlScopeStore implements IFlexScopeStore {
|
|
|
99
115
|
const validated = validateSnapshotSave(snapshot, expectedRevision, assertFlexScopeSnapshot);
|
|
100
116
|
const conflict = (actualRevision: number) =>
|
|
101
117
|
new FlexHarnessStoreConflictError(storageKey, expectedRevision, actualRevision);
|
|
102
|
-
await this.records.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
});
|
|
118
|
+
await this.records.replace(
|
|
119
|
+
{ domain: 'scope', storageKey },
|
|
120
|
+
revisionReplacement(validated, expectedRevision, conflict),
|
|
121
|
+
);
|
|
107
122
|
}
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
/** A projection is a log record: a save writes its change, checked in form. */
|
|
110
126
|
class NoSqlProjectionStore implements IFlexProjectionStore {
|
|
111
127
|
constructor(private readonly records: FlexRecordStore) {}
|
|
112
128
|
|
|
113
|
-
private
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
private log(sessionId: string): IFlexRecordLog<TFlexProjectionSnapshot, IFlexProjectionChange> {
|
|
130
|
+
return {
|
|
131
|
+
validate: (value) => {
|
|
132
|
+
assertFlexProjectionSnapshot(value, sessionId);
|
|
133
|
+
return value;
|
|
134
|
+
},
|
|
135
|
+
validateChange: (value) => {
|
|
136
|
+
assertFlexProjectionChange(value);
|
|
137
|
+
return value;
|
|
138
|
+
},
|
|
139
|
+
apply: applyFlexProjectionChange,
|
|
117
140
|
};
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
public load(storageKey: string, sessionId: string): Promise<TFlexProjectionSnapshot | undefined> {
|
|
121
144
|
providerKey(storageKey, sessionId);
|
|
122
|
-
return this.records.
|
|
123
|
-
{ domain: 'projection', storageKey, sessionId },
|
|
124
|
-
this.validator(sessionId),
|
|
125
|
-
);
|
|
145
|
+
return this.records.readLog({ domain: 'projection', storageKey, sessionId }, this.log(sessionId));
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
public async save(
|
|
129
149
|
storageKey: string,
|
|
130
150
|
sessionId: string,
|
|
131
|
-
|
|
151
|
+
change: IFlexProjectionChange,
|
|
132
152
|
expectedRevision: number,
|
|
133
153
|
): Promise<void> {
|
|
134
154
|
const key = providerKey(storageKey, sessionId);
|
|
135
|
-
const validated =
|
|
136
|
-
|
|
155
|
+
const validated = validateProjectionChangeSave(change, expectedRevision);
|
|
156
|
+
await this.records.append({ domain: 'projection', storageKey, sessionId }, this.log(sessionId), {
|
|
137
157
|
expectedRevision,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
142
|
-
await this.records.mutate({ domain: 'projection', storageKey, sessionId }, {
|
|
143
|
-
validate: this.validator(sessionId),
|
|
144
|
-
decide: (current) => revisionWrite(current, validated, expectedRevision, conflict),
|
|
145
|
-
contention: conflict,
|
|
158
|
+
value: validated,
|
|
159
|
+
conflict: (actualRevision) =>
|
|
160
|
+
new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision),
|
|
146
161
|
});
|
|
147
162
|
}
|
|
148
163
|
|
|
@@ -170,11 +185,10 @@ class NoSqlPermissionStore implements IFlexPermissionStore {
|
|
|
170
185
|
const validated = validateSnapshotSave(snapshot, expectedRevision, assertFlexPermissionSnapshot);
|
|
171
186
|
const conflict = (actualRevision: number) =>
|
|
172
187
|
new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
173
|
-
await this.records.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
188
|
+
await this.records.replace(
|
|
189
|
+
{ domain: 'permission', storageKey, sessionId },
|
|
190
|
+
revisionReplacement(validated, expectedRevision, conflict),
|
|
191
|
+
);
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
public deleteSession(storageKey: string, sessionId: string): Promise<void> {
|
|
@@ -252,9 +266,8 @@ class NoSqlProjectManagementStore implements IFlexProjectManagementStore {
|
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
/** A session's event log is a log record: a save writes its change, checked in form. */
|
|
270
|
+
class NoSqlAgentEventStore implements plugins.IAgentEventStore {
|
|
258
271
|
constructor(
|
|
259
272
|
private readonly records: FlexRecordStore,
|
|
260
273
|
private readonly storageKey: string,
|
|
@@ -269,37 +282,38 @@ class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
269
282
|
}
|
|
270
283
|
}
|
|
271
284
|
|
|
272
|
-
private readonly
|
|
273
|
-
validateAgentEventSnapshot(value, this.sessionId)
|
|
285
|
+
private readonly log: IFlexRecordLog<plugins.IAgentEventSnapshot, plugins.IAgentEventChange> = {
|
|
286
|
+
validate: (value) => validateAgentEventSnapshot(value, this.sessionId),
|
|
287
|
+
validateChange: normalizeAgentEventChange,
|
|
288
|
+
apply: (current, change) => createChangedAgentEventSnapshot(this.sessionId, current, change),
|
|
289
|
+
};
|
|
274
290
|
|
|
275
|
-
public load(sessionId: string): Promise<plugins.
|
|
291
|
+
public load(sessionId: string): Promise<plugins.IAgentEventSnapshot | undefined> {
|
|
276
292
|
this.assertSession(sessionId);
|
|
277
|
-
return this.records.
|
|
293
|
+
return this.records.readLog(
|
|
278
294
|
{ domain: 'agentEvents', storageKey: this.storageKey, sessionId },
|
|
279
|
-
this.
|
|
295
|
+
this.log,
|
|
280
296
|
);
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
public async save(
|
|
284
300
|
sessionId: string,
|
|
285
|
-
|
|
301
|
+
change: plugins.IAgentEventChange,
|
|
286
302
|
expectedRevision: number,
|
|
287
303
|
): Promise<number> {
|
|
288
304
|
this.assertSession(sessionId);
|
|
289
305
|
validateExpectedRevision(expectedRevision);
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
decide: (current) => revisionWrite(current, snapshot, expectedRevision, conflict),
|
|
297
|
-
contention: conflict,
|
|
306
|
+
const normalized = normalizeAgentEventChange(change);
|
|
307
|
+
await this.records.append({ domain: 'agentEvents', storageKey: this.storageKey, sessionId }, this.log, {
|
|
308
|
+
expectedRevision,
|
|
309
|
+
value: normalized,
|
|
310
|
+
conflict: (actualRevision) =>
|
|
311
|
+
new plugins.AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision),
|
|
298
312
|
});
|
|
299
|
-
return
|
|
313
|
+
return expectedRevision + 1;
|
|
300
314
|
}
|
|
301
315
|
|
|
302
|
-
public async archive(archive: plugins.
|
|
316
|
+
public async archive(archive: plugins.IAgentEventArchive): Promise<void> {
|
|
303
317
|
this.assertSession(archive.sessionId);
|
|
304
318
|
const normalized = normalizeJsonObjectUndefined(archive, '$archive');
|
|
305
319
|
const validated = validateAgentEventArchive(normalized, this.sessionId, archive.archiveId);
|
|
@@ -324,7 +338,7 @@ class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
324
338
|
public loadArchive(
|
|
325
339
|
sessionId: string,
|
|
326
340
|
archiveId: string,
|
|
327
|
-
): Promise<plugins.
|
|
341
|
+
): Promise<plugins.IAgentEventArchive | undefined> {
|
|
328
342
|
this.assertSession(sessionId);
|
|
329
343
|
validateIdentifier(archiveId, 'archiveId');
|
|
330
344
|
return this.records.read(
|
|
@@ -387,16 +401,9 @@ class NoSqlToolJobStore implements plugins.IToolJobStore {
|
|
|
387
401
|
const snapshot = createToolJobSnapshot(jobs, expectedRevision + 1);
|
|
388
402
|
const conflict = (actualRevision: number) =>
|
|
389
403
|
new plugins.ToolJobStoreConflictError(expectedRevision, actualRevision);
|
|
390
|
-
await this.records.
|
|
404
|
+
await this.records.replace(
|
|
391
405
|
{ domain: 'jobs', storageKey: this.storageKey, sessionId: this.sessionId },
|
|
392
|
-
|
|
393
|
-
validate: validateToolJobs,
|
|
394
|
-
decide: (current) => {
|
|
395
|
-
abortSignal?.throwIfAborted();
|
|
396
|
-
return revisionWrite(current, snapshot, expectedRevision, conflict);
|
|
397
|
-
},
|
|
398
|
-
contention: conflict,
|
|
399
|
-
},
|
|
406
|
+
revisionReplacement(snapshot, expectedRevision, conflict, abortSignal),
|
|
400
407
|
);
|
|
401
408
|
return snapshot.revision;
|
|
402
409
|
}
|
|
@@ -510,8 +517,9 @@ function validateResult(value: unknown): TJsonValue {
|
|
|
510
517
|
/**
|
|
511
518
|
* Every FlexHarness persistence domain on `@lossless.org/client/nosqldb`, with compare-and-swap
|
|
512
519
|
* that holds across processes. Records live in `flexharness_records`; payloads longer than one
|
|
513
|
-
* chunk live in `flexharness_chunks`;
|
|
514
|
-
*
|
|
520
|
+
* chunk live in `flexharness_chunks`; the changes saved to a projection or an Agent event log
|
|
521
|
+
* since its last compaction live in its head and, beyond the head's inline room, in pages in
|
|
522
|
+
* `flexharness_changes`; `flexharness_namespaces` holds each storage namespace's fence.
|
|
515
523
|
*/
|
|
516
524
|
export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
|
|
517
525
|
public readonly scopes: IFlexScopeStore;
|
|
@@ -538,7 +546,7 @@ export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
|
|
|
538
546
|
}
|
|
539
547
|
|
|
540
548
|
/**
|
|
541
|
-
* Creates or verifies the
|
|
549
|
+
* Creates or verifies the four collections and their indexes. Every store operation awaits the
|
|
542
550
|
* same preparation, so calling it at startup only moves that work, and its failure, forward.
|
|
543
551
|
*/
|
|
544
552
|
public prepare(): Promise<void> {
|
|
@@ -546,7 +554,7 @@ export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
|
|
|
546
554
|
}
|
|
547
555
|
|
|
548
556
|
/**
|
|
549
|
-
* Physically removes every record and
|
|
557
|
+
* Physically removes every record, chunk and change of one storage namespace in every domain: the
|
|
550
558
|
* scope snapshot, each session's projection, permissions, Agent events and archives, jobs and
|
|
551
559
|
* results, and the project-management records and tombstones. This is the host's durable
|
|
552
560
|
* deletion of a scope. Stop new admission for every alias of the scope first. Without `fence`,
|