@modelprofile.com/flexharness 11.3.1 → 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 +44 -25
  25. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +152 -20
  26. package/dist_ts_stores_nosqldb/classes.recordstore.js +369 -66
  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 +59 -46
  57. package/ts_stores_nosqldb/classes.recordstore.ts +445 -64
  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,7 +55,11 @@ import {
49
55
  validateResultIndex,
50
56
  validateSnapshotSave,
51
57
  } from '../ts/utils.storerules.js';
52
- import { FlexRecordStore, type IFlexRecordReplacement } from './classes.recordstore.js';
58
+ import {
59
+ FlexRecordStore,
60
+ type IFlexRecordLog,
61
+ type IFlexRecordReplacement,
62
+ } from './classes.recordstore.js';
53
63
 
54
64
  /** Replaces a record with a snapshot while the record's revision is the expected one. */
55
65
  function revisionReplacement<TSnapshot extends { revision: number }>(
@@ -112,42 +122,43 @@ class NoSqlScopeStore implements IFlexScopeStore {
112
122
  }
113
123
  }
114
124
 
125
+ /** A projection is a log record: a save writes its change, checked in form. */
115
126
  class NoSqlProjectionStore implements IFlexProjectionStore {
116
127
  constructor(private readonly records: FlexRecordStore) {}
117
128
 
118
- private validator(sessionId: string): (value: unknown) => TFlexProjectionSnapshot {
119
- return (value) => {
120
- assertFlexProjectionSnapshot(value, sessionId);
121
- 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,
122
140
  };
123
141
  }
124
142
 
125
143
  public load(storageKey: string, sessionId: string): Promise<TFlexProjectionSnapshot | undefined> {
126
144
  providerKey(storageKey, sessionId);
127
- return this.records.read(
128
- { domain: 'projection', storageKey, sessionId },
129
- this.validator(sessionId),
130
- );
145
+ return this.records.readLog({ domain: 'projection', storageKey, sessionId }, this.log(sessionId));
131
146
  }
132
147
 
133
148
  public async save(
134
149
  storageKey: string,
135
150
  sessionId: string,
136
- snapshot: IFlexProjectionSnapshotV3,
151
+ change: IFlexProjectionChange,
137
152
  expectedRevision: number,
138
153
  ): Promise<void> {
139
154
  const key = providerKey(storageKey, sessionId);
140
- const validated = validateSnapshotSave(
141
- snapshot,
155
+ const validated = validateProjectionChangeSave(change, expectedRevision);
156
+ await this.records.append({ domain: 'projection', storageKey, sessionId }, this.log(sessionId), {
142
157
  expectedRevision,
143
- (value) => assertFlexProjectionSnapshot(value, sessionId),
144
- );
145
- const conflict = (actualRevision: number) =>
146
- new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
147
- await this.records.replace(
148
- { domain: 'projection', storageKey, sessionId },
149
- revisionReplacement(validated, expectedRevision, conflict),
150
- );
158
+ value: validated,
159
+ conflict: (actualRevision) =>
160
+ new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision),
161
+ });
151
162
  }
152
163
 
153
164
  public deleteSession(storageKey: string, sessionId: string): Promise<void> {
@@ -255,9 +266,8 @@ class NoSqlProjectManagementStore implements IFlexProjectManagementStore {
255
266
  }
256
267
  }
257
268
 
258
- class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
259
- public readonly eventSchemaVersion = 2 as const;
260
-
269
+ /** A session's event log is a log record: a save writes its change, checked in form. */
270
+ class NoSqlAgentEventStore implements plugins.IAgentEventStore {
261
271
  constructor(
262
272
  private readonly records: FlexRecordStore,
263
273
  private readonly storageKey: string,
@@ -272,36 +282,38 @@ class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
272
282
  }
273
283
  }
274
284
 
275
- private readonly validateSnapshot = (value: unknown): plugins.IAgentEventSnapshotV2 =>
276
- 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
+ };
277
290
 
278
- public load(sessionId: string): Promise<plugins.IAgentEventSnapshotV2 | undefined> {
291
+ public load(sessionId: string): Promise<plugins.IAgentEventSnapshot | undefined> {
279
292
  this.assertSession(sessionId);
280
- return this.records.read(
293
+ return this.records.readLog(
281
294
  { domain: 'agentEvents', storageKey: this.storageKey, sessionId },
282
- this.validateSnapshot,
295
+ this.log,
283
296
  );
284
297
  }
285
298
 
286
299
  public async save(
287
300
  sessionId: string,
288
- events: readonly plugins.TAgentEvent[],
301
+ change: plugins.IAgentEventChange,
289
302
  expectedRevision: number,
290
303
  ): Promise<number> {
291
304
  this.assertSession(sessionId);
292
305
  validateExpectedRevision(expectedRevision);
293
- const snapshot = createAgentEventSnapshot(sessionId, events, expectedRevision + 1);
294
- validateAgentEventSnapshot(snapshot, sessionId);
295
- const conflict = (actualRevision: number) =>
296
- new plugins.AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision);
297
- await this.records.replace(
298
- { domain: 'agentEvents', storageKey: this.storageKey, sessionId },
299
- revisionReplacement(snapshot, expectedRevision, conflict),
300
- );
301
- return snapshot.revision;
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),
312
+ });
313
+ return expectedRevision + 1;
302
314
  }
303
315
 
304
- public async archive(archive: plugins.IAgentEventArchiveV2): Promise<void> {
316
+ public async archive(archive: plugins.IAgentEventArchive): Promise<void> {
305
317
  this.assertSession(archive.sessionId);
306
318
  const normalized = normalizeJsonObjectUndefined(archive, '$archive');
307
319
  const validated = validateAgentEventArchive(normalized, this.sessionId, archive.archiveId);
@@ -326,7 +338,7 @@ class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
326
338
  public loadArchive(
327
339
  sessionId: string,
328
340
  archiveId: string,
329
- ): Promise<plugins.IAgentEventArchiveV2 | undefined> {
341
+ ): Promise<plugins.IAgentEventArchive | undefined> {
330
342
  this.assertSession(sessionId);
331
343
  validateIdentifier(archiveId, 'archiveId');
332
344
  return this.records.read(
@@ -505,8 +517,9 @@ function validateResult(value: unknown): TJsonValue {
505
517
  /**
506
518
  * Every FlexHarness persistence domain on `@lossless.org/client/nosqldb`, with compare-and-swap
507
519
  * that holds across processes. Records live in `flexharness_records`; payloads longer than one
508
- * chunk live in `flexharness_chunks`; `flexharness_namespaces` holds each storage namespace's
509
- * 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.
510
523
  */
511
524
  export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
512
525
  public readonly scopes: IFlexScopeStore;
@@ -533,7 +546,7 @@ export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
533
546
  }
534
547
 
535
548
  /**
536
- * 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
537
550
  * same preparation, so calling it at startup only moves that work, and its failure, forward.
538
551
  */
539
552
  public prepare(): Promise<void> {
@@ -541,7 +554,7 @@ export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
541
554
  }
542
555
 
543
556
  /**
544
- * 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
545
558
  * scope snapshot, each session's projection, permissions, Agent events and archives, jobs and
546
559
  * results, and the project-management records and tombstones. This is the host's durable
547
560
  * deletion of a scope. Stop new admission for every alias of the scope first. Without `fence`,