@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.
Files changed (65) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.core.d.ts +4 -0
  3. package/dist_ts/classes.flexharness.core.js +69 -25
  4. package/dist_ts/classes.flexharness.imports.js +4 -3
  5. package/dist_ts/classes.flexharness.reversions.d.ts +7 -1
  6. package/dist_ts/classes.flexharness.reversions.js +43 -29
  7. package/dist_ts/classes.flexharness.sessions.js +2 -6
  8. package/dist_ts/classes.stores.js +20 -18
  9. package/dist_ts/index.d.ts +1 -0
  10. package/dist_ts/index.js +2 -1
  11. package/dist_ts/interfaces.d.ts +24 -2
  12. package/dist_ts/interfaces.flexharness.d.ts +8 -1
  13. package/dist_ts/interfaces.js +1 -1
  14. package/dist_ts/plugins.d.ts +3 -3
  15. package/dist_ts/plugins.js +3 -3
  16. package/dist_ts/utils.projectionchange.d.ts +34 -0
  17. package/dist_ts/utils.projectionchange.js +135 -0
  18. package/dist_ts/utils.storecodecs.d.ts +10 -3
  19. package/dist_ts/utils.storecodecs.js +29 -4
  20. package/dist_ts/utils.storerules.d.ts +1 -1
  21. package/dist_ts/utils.storerules.js +1 -1
  22. package/dist_ts_migration/v3_legacyflexharness.js +6 -5
  23. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +5 -4
  24. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +56 -54
  25. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +176 -19
  26. package/dist_ts_stores_nosqldb/classes.recordstore.js +392 -67
  27. package/dist_ts_stores_nosqldb/index.d.ts +1 -1
  28. package/dist_ts_stores_nosqldb/index.js +2 -2
  29. package/dist_ts_stores_nosqldb/plugins.d.ts +1 -1
  30. package/dist_ts_stores_nosqldb/plugins.js +1 -1
  31. package/dist_ts_stores_testing/contract.d.ts +33 -0
  32. package/dist_ts_stores_testing/contract.js +566 -0
  33. package/dist_ts_stores_testing/fixtures.d.ts +20 -0
  34. package/dist_ts_stores_testing/fixtures.js +112 -0
  35. package/dist_ts_stores_testing/index.d.ts +2 -0
  36. package/dist_ts_stores_testing/index.js +3 -0
  37. package/dist_ts_stores_testing/plugins.d.ts +5 -0
  38. package/dist_ts_stores_testing/plugins.js +7 -0
  39. package/package.json +9 -3
  40. package/readme.md +6 -3
  41. package/ts/00_commitinfo_data.ts +1 -1
  42. package/ts/classes.flexharness.core.ts +74 -24
  43. package/ts/classes.flexharness.imports.ts +3 -2
  44. package/ts/classes.flexharness.reversions.ts +53 -28
  45. package/ts/classes.flexharness.sessions.ts +2 -5
  46. package/ts/classes.stores.ts +40 -37
  47. package/ts/index.ts +5 -0
  48. package/ts/interfaces.flexharness.ts +8 -1
  49. package/ts/interfaces.ts +27 -2
  50. package/ts/plugins.ts +12 -7
  51. package/ts/readme.md +6 -3
  52. package/ts/utils.projectionchange.ts +190 -0
  53. package/ts/utils.storecodecs.ts +42 -7
  54. package/ts/utils.storerules.ts +2 -2
  55. package/ts_migration/v3_legacyflexharness.ts +11 -6
  56. package/ts_stores_nosqldb/classes.nosqldbstores.ts +79 -71
  57. package/ts_stores_nosqldb/classes.recordstore.ts +491 -66
  58. package/ts_stores_nosqldb/index.ts +1 -0
  59. package/ts_stores_nosqldb/plugins.ts +4 -3
  60. package/ts_stores_nosqldb/readme.md +6 -2
  61. package/ts_stores_testing/contract.ts +776 -0
  62. package/ts_stores_testing/fixtures.ts +127 -0
  63. package/ts_stores_testing/index.ts +17 -0
  64. package/ts_stores_testing/plugins.ts +20 -0
  65. package/ts_stores_testing/readme.md +62 -0
@@ -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.IAgentEventArchiveV2,
148
- next: plugins.IAgentEventArchiveV2,
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.IAgentEventStoreV2;
63
- eventSnapshot: plugins.IAgentEventSnapshotV2 | undefined;
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.validateAgentEventSnapshotV2({
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: eventSnapshot as plugins.IAgentEventSnapshotV2 | undefined,
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(session.sessionId, session.agentEvents, 0);
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
- IFlexProjectionSnapshotV3,
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
- createAgentEventSnapshot,
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 { FlexRecordStore, type IFlexRecordWrite } from './classes.recordstore.js';
58
+ import {
59
+ FlexRecordStore,
60
+ type IFlexRecordLog,
61
+ type IFlexRecordReplacement,
62
+ } from './classes.recordstore.js';
53
63
 
54
- function revisionWrite<TSnapshot extends { revision: number }>(
55
- current: { revision: number } | undefined,
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
- ): IFlexRecordWrite {
60
- const actualRevision = current?.revision ?? 0;
61
- if (actualRevision !== expectedRevision) throw conflict(actualRevision);
62
- return { revision: snapshot.revision, value: snapshot };
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.mutate({ domain: 'scope', storageKey }, {
103
- validate: validateScope,
104
- decide: (current) => revisionWrite(current, validated, expectedRevision, conflict),
105
- contention: conflict,
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 validator(sessionId: string): (value: unknown) => TFlexProjectionSnapshot {
114
- return (value) => {
115
- assertFlexProjectionSnapshot(value, sessionId);
116
- return value;
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.read(
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
- snapshot: IFlexProjectionSnapshotV3,
151
+ change: IFlexProjectionChange,
132
152
  expectedRevision: number,
133
153
  ): Promise<void> {
134
154
  const key = providerKey(storageKey, sessionId);
135
- const validated = validateSnapshotSave(
136
- snapshot,
155
+ const validated = validateProjectionChangeSave(change, expectedRevision);
156
+ await this.records.append({ domain: 'projection', storageKey, sessionId }, this.log(sessionId), {
137
157
  expectedRevision,
138
- (value) => assertFlexProjectionSnapshot(value, sessionId),
139
- );
140
- const conflict = (actualRevision: number) =>
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.mutate({ domain: 'permission', storageKey, sessionId }, {
174
- validate: validatePermission,
175
- decide: (current) => revisionWrite(current, validated, expectedRevision, conflict),
176
- contention: conflict,
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
- class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
256
- public readonly eventSchemaVersion = 2 as const;
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 validateSnapshot = (value: unknown): plugins.IAgentEventSnapshotV2 =>
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.IAgentEventSnapshotV2 | undefined> {
291
+ public load(sessionId: string): Promise<plugins.IAgentEventSnapshot | undefined> {
276
292
  this.assertSession(sessionId);
277
- return this.records.read(
293
+ return this.records.readLog(
278
294
  { domain: 'agentEvents', storageKey: this.storageKey, sessionId },
279
- this.validateSnapshot,
295
+ this.log,
280
296
  );
281
297
  }
282
298
 
283
299
  public async save(
284
300
  sessionId: string,
285
- events: readonly plugins.TAgentEvent[],
301
+ change: plugins.IAgentEventChange,
286
302
  expectedRevision: number,
287
303
  ): Promise<number> {
288
304
  this.assertSession(sessionId);
289
305
  validateExpectedRevision(expectedRevision);
290
- const snapshot = createAgentEventSnapshot(sessionId, events, expectedRevision + 1);
291
- validateAgentEventSnapshot(snapshot, sessionId);
292
- const conflict = (actualRevision: number) =>
293
- new plugins.AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision);
294
- await this.records.mutate({ domain: 'agentEvents', storageKey: this.storageKey, sessionId }, {
295
- validate: this.validateSnapshot,
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 snapshot.revision;
313
+ return expectedRevision + 1;
300
314
  }
301
315
 
302
- public async archive(archive: plugins.IAgentEventArchiveV2): Promise<void> {
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.IAgentEventArchiveV2 | undefined> {
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.mutate(
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`; `flexharness_namespaces` holds each storage namespace's
514
- * fence.
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 three collections and their indexes. Every store operation awaits 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 chunk of one storage namespace in every domain: the
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`,