@lmzhen/dsh-evolution-state-storage 0.25.9 → 0.26.26
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/lib/index.js +99 -1
- package/lib/types/index.d.ts +11 -0
- package/lib/types/medium-comparison.d.ts +36 -0
- package/lib/types/medium-declaration.d.ts +40 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -427,6 +427,93 @@ async function runStateProviderConsistency(provider, assert, options = {}) {
|
|
|
427
427
|
assert(await provider.loadReviewState(sessionPrefix + "0")).toEqual(reviewStateOf(999));
|
|
428
428
|
}
|
|
429
429
|
//#endregion
|
|
430
|
+
//#region lib/types/medium-comparison.js
|
|
431
|
+
/**
|
|
432
|
+
* Compare the pending state as two media read it.
|
|
433
|
+
* @param left - the provider currently in use.
|
|
434
|
+
* @param right - the provider the deployment would switch to.
|
|
435
|
+
* @returns whether the readings agree, and every disagreement, ordered by id.
|
|
436
|
+
*/
|
|
437
|
+
async function comparePendingReadings(left, right) {
|
|
438
|
+
const [leftRecords, rightRecords] = await Promise.all([left.listPending(), right.listPending()]);
|
|
439
|
+
const statusOf = (records) => new Map(records.map((record) => [record.id, record.status]));
|
|
440
|
+
const leftStatus = statusOf(leftRecords);
|
|
441
|
+
const rightStatus = statusOf(rightRecords);
|
|
442
|
+
const ids = [...new Set([...leftStatus.keys(), ...rightStatus.keys()])].sort();
|
|
443
|
+
const differences = [];
|
|
444
|
+
for (const id of ids) {
|
|
445
|
+
const leftValue = leftStatus.get(id);
|
|
446
|
+
const rightValue = rightStatus.get(id);
|
|
447
|
+
if (leftValue === rightValue) continue;
|
|
448
|
+
const verdict = leftValue === void 0 ? "right-only" : rightValue === void 0 ? "left-only" : "status-differs";
|
|
449
|
+
differences.push({
|
|
450
|
+
id,
|
|
451
|
+
left: leftValue,
|
|
452
|
+
right: rightValue,
|
|
453
|
+
verdict
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
consistent: differences.length === 0,
|
|
458
|
+
differences
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region lib/types/medium-declaration.js
|
|
463
|
+
/**
|
|
464
|
+
* G2b/2b.3: which medium carries each record type, and what PROCESS SCOPE that medium actually gives.
|
|
465
|
+
*
|
|
466
|
+
* The family has two serialization layers and only ever wrote one of them down, for the json provider: the
|
|
467
|
+
* IO backend cross-process transact lock (first layer) and the caller own in-process chain (second). The
|
|
468
|
+
* domain medium inherits its answer from whatever backend the platform storage facility routes, and nothing
|
|
469
|
+
* stated it. So this file states it - and the criterion is the family one: a CLAIM must be produced by a
|
|
470
|
+
* capability that can be DISPROVED. processScopeViolations takes the io the provider writes through and
|
|
471
|
+
* reports every claim of cross-process that the io cannot back up.
|
|
472
|
+
*
|
|
473
|
+
* MIGRATION NOTE: the plan allows naming storage-sqlite for a cross-process record type. That package does
|
|
474
|
+
* not exist in this tree (0 hits), so today every entry is either backed by the IO transact lock or
|
|
475
|
+
* declares single-process. When a backend carrying its own cross-process guarantee lands, it is named here
|
|
476
|
+
* and the reasoning moves with it - one declaration, not two.
|
|
477
|
+
*
|
|
478
|
+
* @module @lmzhen/dsh-evolution-state-storage/medium-declaration
|
|
479
|
+
*/
|
|
480
|
+
/** The declaration. */
|
|
481
|
+
const MEDIUM_PROCESS_DECLARATION = [
|
|
482
|
+
{
|
|
483
|
+
record: "review_state",
|
|
484
|
+
medium: "json",
|
|
485
|
+
scope: "cross-process",
|
|
486
|
+
because: "the json provider serializes through the IO backend transact lock, as its README states"
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
record: "curator_state",
|
|
490
|
+
medium: "json",
|
|
491
|
+
scope: "cross-process",
|
|
492
|
+
because: "same provider and same lock as review_state; the run bookkeeping rides the same write path"
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
record: "pending",
|
|
496
|
+
medium: "json",
|
|
497
|
+
scope: "cross-process",
|
|
498
|
+
because: "a staged write is claimed by name across processes, so it needs the lock more than the other two"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
record: "evolution-domain",
|
|
502
|
+
medium: "domain",
|
|
503
|
+
scope: "single-process",
|
|
504
|
+
because: "no backend in this tree carries its own cross-process guarantee and none is named, so the domain facility lock semantics stay the backend business and this declares the weaker truth"
|
|
505
|
+
}
|
|
506
|
+
];
|
|
507
|
+
/**
|
|
508
|
+
* Falsify the declaration against a capability reading.
|
|
509
|
+
* @param io - the io backend the medium writes through, or undefined when none is mounted.
|
|
510
|
+
* @returns one message per claim the capability cannot back up; empty when every claim holds.
|
|
511
|
+
*/
|
|
512
|
+
function processScopeViolations(io) {
|
|
513
|
+
const backed = typeof io?.transact === "function";
|
|
514
|
+
return MEDIUM_PROCESS_DECLARATION.filter((claim) => claim.scope === "cross-process" && !backed).map((claim) => claim.record + " claims cross-process (" + claim.because + ") but the mounted io exposes no transact lock");
|
|
515
|
+
}
|
|
516
|
+
//#endregion
|
|
430
517
|
//#region lib/types/index.js
|
|
431
518
|
/**
|
|
432
519
|
* Provider seam for durable evolution state.
|
|
@@ -534,6 +621,17 @@ var EvolutionStateStorageRegistry = class extends Service {
|
|
|
534
621
|
hasProviders() {
|
|
535
622
|
return this.providers.size > 0;
|
|
536
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* G2b/2b.1: the registered provider names, in registration order.
|
|
626
|
+
*
|
|
627
|
+
* A consumer needs the COUNT to refuse an unpinned choice while more than one medium is mounted:
|
|
628
|
+
* this registry cannot make that call itself, because the pin lives in the consumer's config and the
|
|
629
|
+
* registry never sees it. Read-only, and it exists so the refusal can happen at mount instead of at the
|
|
630
|
+
* first read (where the state is already half-consumed and the message arrives too late to act on).
|
|
631
|
+
*/
|
|
632
|
+
providerNames() {
|
|
633
|
+
return [...this.providers.keys()];
|
|
634
|
+
}
|
|
537
635
|
provider(name) {
|
|
538
636
|
if (name) {
|
|
539
637
|
this.ambiguousWarned = false;
|
|
@@ -553,4 +651,4 @@ var EvolutionStateStorageRegistry = class extends Service {
|
|
|
553
651
|
}
|
|
554
652
|
};
|
|
555
653
|
//#endregion
|
|
556
|
-
export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_KINDS, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_STATUSES, PENDING_TABLE, PROVIDER_DOMAIN, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, UNKNOWN_FIELD_POLICY, assertCloneable, canClaimPending, canResolvePending, cloneRecord, recordIssue, releasedStatus, runStateProviderConsistency, selectPendingOverflow, selectSessionOverflow };
|
|
654
|
+
export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, MEDIUM_PROCESS_DECLARATION, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_KINDS, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_STATUSES, PENDING_TABLE, PROVIDER_DOMAIN, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, UNKNOWN_FIELD_POLICY, assertCloneable, canClaimPending, canResolvePending, cloneRecord, comparePendingReadings, processScopeViolations, recordIssue, releasedStatus, runStateProviderConsistency, selectPendingOverflow, selectSessionOverflow };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { PendingKind, PendingStatus } from './record-contract.ts';
|
|
|
11
11
|
export * from './constants.ts';
|
|
12
12
|
export * from './record-contract.ts';
|
|
13
13
|
export * from './conformance.ts';
|
|
14
|
+
export * from './medium-comparison.ts';
|
|
15
|
+
export * from './medium-declaration.ts';
|
|
14
16
|
/** 0.3.17 (S3.3): the claim lifecycle as ONE transition table — BOTH
|
|
15
17
|
* providers (json/domain) must use these, never a hand-written copy (a second
|
|
16
18
|
* copy is exactly the E-10 drift class). */
|
|
@@ -163,6 +165,15 @@ export declare class EvolutionStateStorageRegistry extends Service {
|
|
|
163
165
|
* at start with the registry's precise message) while staying lazy when no
|
|
164
166
|
* provider has registered yet (mount order not settled). */
|
|
165
167
|
hasProviders(): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* G2b/2b.1: the registered provider names, in registration order.
|
|
170
|
+
*
|
|
171
|
+
* A consumer needs the COUNT to refuse an unpinned choice while more than one medium is mounted:
|
|
172
|
+
* this registry cannot make that call itself, because the pin lives in the consumer's config and the
|
|
173
|
+
* registry never sees it. Read-only, and it exists so the refusal can happen at mount instead of at the
|
|
174
|
+
* first read (where the state is already half-consumed and the message arrives too late to act on).
|
|
175
|
+
*/
|
|
176
|
+
providerNames(): readonly string[];
|
|
166
177
|
provider(name?: string): EvolutionStateStorage;
|
|
167
178
|
}
|
|
168
179
|
export default EvolutionStateStorageRegistry;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G2a/2a.2: the two-medium read comparison.
|
|
3
|
+
*
|
|
4
|
+
* Audit S1-B02: switching back to the json medium revives staged records the other medium never had
|
|
5
|
+
* (the json read face merges a legacy `pending.json`, which re-introduces an evicted record), and the
|
|
6
|
+
* default TTL of 0 makes age no threshold at all. The fix is not a third store - it is being able to SEE
|
|
7
|
+
* the disagreement: the SAME logical state read through both providers must agree.
|
|
8
|
+
*
|
|
9
|
+
* This lives in the seam package because that is the layer the two providers share. Comparing the
|
|
10
|
+
* packages' own file reads would bypass the seam and stop working the moment a provider is swapped.
|
|
11
|
+
*
|
|
12
|
+
* Read-only: it reports, it never writes and never repairs.
|
|
13
|
+
*/
|
|
14
|
+
import type { EvolutionStateStorage, PendingStatus } from './index.ts';
|
|
15
|
+
/** One record that the two readings do not agree on. */
|
|
16
|
+
export interface MediumDifference {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly left: PendingStatus | undefined;
|
|
19
|
+
readonly right: PendingStatus | undefined;
|
|
20
|
+
/** `left-only` is the revival shape: the json side holds a record the other medium never had. */
|
|
21
|
+
readonly verdict: 'left-only' | 'right-only' | 'status-differs';
|
|
22
|
+
}
|
|
23
|
+
/** The comparison result. `consistent: false` is the red signal S1-B02 asks for. */
|
|
24
|
+
export interface MediumComparison {
|
|
25
|
+
readonly consistent: boolean;
|
|
26
|
+
/** One entry per record the readings disagree on, ordered by id. */
|
|
27
|
+
readonly differences: readonly MediumDifference[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compare the pending state as two media read it.
|
|
31
|
+
* @param left - the provider currently in use.
|
|
32
|
+
* @param right - the provider the deployment would switch to.
|
|
33
|
+
* @returns whether the readings agree, and every disagreement, ordered by id.
|
|
34
|
+
*/
|
|
35
|
+
export declare function comparePendingReadings(left: Pick<EvolutionStateStorage, 'listPending'>, right: Pick<EvolutionStateStorage, 'listPending'>): Promise<MediumComparison>;
|
|
36
|
+
//# sourceMappingURL=medium-comparison.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G2b/2b.3: which medium carries each record type, and what PROCESS SCOPE that medium actually gives.
|
|
3
|
+
*
|
|
4
|
+
* The family has two serialization layers and only ever wrote one of them down, for the json provider: the
|
|
5
|
+
* IO backend cross-process transact lock (first layer) and the caller own in-process chain (second). The
|
|
6
|
+
* domain medium inherits its answer from whatever backend the platform storage facility routes, and nothing
|
|
7
|
+
* stated it. So this file states it - and the criterion is the family one: a CLAIM must be produced by a
|
|
8
|
+
* capability that can be DISPROVED. processScopeViolations takes the io the provider writes through and
|
|
9
|
+
* reports every claim of cross-process that the io cannot back up.
|
|
10
|
+
*
|
|
11
|
+
* MIGRATION NOTE: the plan allows naming storage-sqlite for a cross-process record type. That package does
|
|
12
|
+
* not exist in this tree (0 hits), so today every entry is either backed by the IO transact lock or
|
|
13
|
+
* declares single-process. When a backend carrying its own cross-process guarantee lands, it is named here
|
|
14
|
+
* and the reasoning moves with it - one declaration, not two.
|
|
15
|
+
*
|
|
16
|
+
* @module @lmzhen/dsh-evolution-state-storage/medium-declaration
|
|
17
|
+
*/
|
|
18
|
+
/** One record type: its medium, the process scope claimed for it, and why that is true. */
|
|
19
|
+
export interface MediumProcessClaim {
|
|
20
|
+
/** The record family, named as the domain declares it. */
|
|
21
|
+
readonly record: string;
|
|
22
|
+
readonly medium: 'json' | 'domain';
|
|
23
|
+
/** cross-process is a CLAIM: processScopeViolations refuses it without a backing transact. */
|
|
24
|
+
readonly scope: 'cross-process' | 'single-process';
|
|
25
|
+
readonly because: string;
|
|
26
|
+
}
|
|
27
|
+
/** The declaration. */
|
|
28
|
+
export declare const MEDIUM_PROCESS_DECLARATION: readonly MediumProcessClaim[];
|
|
29
|
+
/** The capability the claims are checked against: the io the medium writes through. */
|
|
30
|
+
export interface ProcessScopeProbe {
|
|
31
|
+
/** The IO backend transact entry point, when it has one. */
|
|
32
|
+
readonly transact?: unknown;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Falsify the declaration against a capability reading.
|
|
36
|
+
* @param io - the io backend the medium writes through, or undefined when none is mounted.
|
|
37
|
+
* @returns one message per claim the capability cannot back up; empty when every claim holds.
|
|
38
|
+
*/
|
|
39
|
+
export declare function processScopeViolations(io: ProcessScopeProbe | undefined): readonly string[];
|
|
40
|
+
//# sourceMappingURL=medium-declaration.d.ts.map
|