@lmzhen/dsh-evolution-state-storage 0.3.67 → 0.3.69
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 +64 -1
- package/lib/types/index.d.ts +52 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -170,10 +170,66 @@ const PENDING_RESOLVED_CAP = 200;
|
|
|
170
170
|
* interface change, no background sweeper).
|
|
171
171
|
*/
|
|
172
172
|
const REVIEW_STATE_SESSION_CAP = 500;
|
|
173
|
+
/**
|
|
174
|
+
* V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
|
|
175
|
+
* providers apply (the two implementations had drifted into separate files and
|
|
176
|
+
* only one of them was ever updated by a later fix).
|
|
177
|
+
*
|
|
178
|
+
* Eligible = resolved (`approved`/`rejected`) and NOT a capability approval:
|
|
179
|
+
* v23 (AP-1) exempts those because the Creator-mode contract reads the LIVE
|
|
180
|
+
* approved list and a capability cannot be re-submitted for the same package,
|
|
181
|
+
* so eviction would make an approved capability permanently unactivatable.
|
|
182
|
+
* Ordering = oldest `resolvedAt` first; a missing or unparseable timestamp sorts
|
|
183
|
+
* LAST (json parity, v16), and pending/executing rows are live work that is
|
|
184
|
+
* never trimmed. v28 G2.4 (STATE-03) — the previous wording promised "an
|
|
185
|
+
* unknown time must never make a record the victim"; that is only true while
|
|
186
|
+
* the overflow fits within the KNOWN-timestamp records. When the overflow
|
|
187
|
+
* exceeds them (corruption-level data: most resolved rows with broken
|
|
188
|
+
* timestamps), `slice` necessarily reaches into the unknown-timestamp tail —
|
|
189
|
+
* the cap must be enforced, so there is no alternative. The guarantee is
|
|
190
|
+
* therefore: unknown-timestamp records are evicted LAST, after every known
|
|
191
|
+
* timestamp, in insertion order among themselves (Array#sort stability).
|
|
192
|
+
*
|
|
193
|
+
* @param records - every pending record currently in the live table.
|
|
194
|
+
* @param cap - how many resolved records may be kept.
|
|
195
|
+
* @returns the records to evict, oldest first (empty when within the cap).
|
|
196
|
+
*/
|
|
197
|
+
function selectPendingOverflow(records, cap = 200) {
|
|
198
|
+
const resolved = records.filter((record) => (record.status === "approved" || record.status === "rejected") && record.kind !== "capability");
|
|
199
|
+
const overflow = resolved.length - cap;
|
|
200
|
+
if (overflow <= 0) return [];
|
|
201
|
+
const resolvedAtMs = (record) => {
|
|
202
|
+
if (!record.resolvedAt) return Number.MAX_SAFE_INTEGER;
|
|
203
|
+
const parsed = Date.parse(record.resolvedAt);
|
|
204
|
+
return Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed;
|
|
205
|
+
};
|
|
206
|
+
return [...resolved].sort((a, b) => resolvedAtMs(a) - resolvedAtMs(b)).slice(0, overflow);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* V27 G2.2: which review-state session rows a save evicts, as one pure rule
|
|
210
|
+
* both providers apply. The saving session is never a candidate (it is the most
|
|
211
|
+
* recent write by definition); the rest are ordered by their provider stamp
|
|
212
|
+
* ascending, with a missing stamp read as 0 = oldest, because an active session
|
|
213
|
+
* re-stamps its row on its next save and an unknown stamp is stale by
|
|
214
|
+
* construction. Exactly one row is dropped per over-cap save, which keeps the
|
|
215
|
+
* table at the cap in steady state.
|
|
216
|
+
*
|
|
217
|
+
* @param rows - the other sessions' rows, with their stamps.
|
|
218
|
+
* @param cap - how many session rows may exist.
|
|
219
|
+
* @returns the keys to delete, oldest first.
|
|
220
|
+
*/
|
|
221
|
+
function selectSessionOverflow(rows, options, cap = 500) {
|
|
222
|
+
const overflow = rows.length - cap + 1;
|
|
223
|
+
if (overflow <= 0) return [];
|
|
224
|
+
return [...rows].sort((a, b) => options.stampOf(a) - options.stampOf(b)).slice(0, overflow).map((row) => options.keyOf(row));
|
|
225
|
+
}
|
|
173
226
|
var EvolutionStateStorageRegistry = class extends Service {
|
|
174
227
|
providers = /* @__PURE__ */ new Map();
|
|
175
228
|
/** C-7 (v18): per-name dispose, mirroring the evolution-io registry. */
|
|
176
229
|
disposals = /* @__PURE__ */ new Map();
|
|
230
|
+
/** v28 G3.1 (STATE-04): the multi-provider warn fires once per ambiguous
|
|
231
|
+
* period, not once per state op (provider() runs on every read/write). */
|
|
232
|
+
ambiguousWarned = false;
|
|
177
233
|
constructor(ctx) {
|
|
178
234
|
super(ctx, "evolutionStateStorage");
|
|
179
235
|
}
|
|
@@ -206,14 +262,21 @@ var EvolutionStateStorageRegistry = class extends Service {
|
|
|
206
262
|
}
|
|
207
263
|
provider(name) {
|
|
208
264
|
if (name) {
|
|
265
|
+
this.ambiguousWarned = false;
|
|
209
266
|
const provider = this.providers.get(name);
|
|
210
267
|
if (provider) return provider;
|
|
211
268
|
throw new Error(`evolution state storage provider "${name}" is not registered`);
|
|
212
269
|
}
|
|
213
270
|
const first = this.providers.values().next().value;
|
|
214
271
|
if (!first) throw new Error("no evolution state storage provider registered; mount @lmzhen/dsh-evolution-state-json or @lmzhen/dsh-evolution-state-domain");
|
|
272
|
+
if (this.providers.size > 1) {
|
|
273
|
+
if (!this.ambiguousWarned) {
|
|
274
|
+
this.ambiguousWarned = true;
|
|
275
|
+
this.ctx.logger.warn(`evolution state storage: ${this.providers.size} providers registered (${[...this.providers.keys()].join(", ")}), none pinned — the effective provider is "${first.name}" (registration order). Existing state under the other provider is now invisible. Pin config: { provider: json|domain } on the evolution-state row to make the choice explicit.`);
|
|
276
|
+
}
|
|
277
|
+
} else this.ambiguousWarned = false;
|
|
215
278
|
return first;
|
|
216
279
|
}
|
|
217
280
|
};
|
|
218
281
|
//#endregion
|
|
219
|
-
export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, 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 };
|
|
282
|
+
export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, 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, selectPendingOverflow, selectSessionOverflow };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -60,6 +60,48 @@ export declare const PENDING_RESOLVED_CAP = 200;
|
|
|
60
60
|
* interface change, no background sweeper).
|
|
61
61
|
*/
|
|
62
62
|
export declare const REVIEW_STATE_SESSION_CAP = 500;
|
|
63
|
+
/**
|
|
64
|
+
* V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
|
|
65
|
+
* providers apply (the two implementations had drifted into separate files and
|
|
66
|
+
* only one of them was ever updated by a later fix).
|
|
67
|
+
*
|
|
68
|
+
* Eligible = resolved (`approved`/`rejected`) and NOT a capability approval:
|
|
69
|
+
* v23 (AP-1) exempts those because the Creator-mode contract reads the LIVE
|
|
70
|
+
* approved list and a capability cannot be re-submitted for the same package,
|
|
71
|
+
* so eviction would make an approved capability permanently unactivatable.
|
|
72
|
+
* Ordering = oldest `resolvedAt` first; a missing or unparseable timestamp sorts
|
|
73
|
+
* LAST (json parity, v16), and pending/executing rows are live work that is
|
|
74
|
+
* never trimmed. v28 G2.4 (STATE-03) — the previous wording promised "an
|
|
75
|
+
* unknown time must never make a record the victim"; that is only true while
|
|
76
|
+
* the overflow fits within the KNOWN-timestamp records. When the overflow
|
|
77
|
+
* exceeds them (corruption-level data: most resolved rows with broken
|
|
78
|
+
* timestamps), `slice` necessarily reaches into the unknown-timestamp tail —
|
|
79
|
+
* the cap must be enforced, so there is no alternative. The guarantee is
|
|
80
|
+
* therefore: unknown-timestamp records are evicted LAST, after every known
|
|
81
|
+
* timestamp, in insertion order among themselves (Array#sort stability).
|
|
82
|
+
*
|
|
83
|
+
* @param records - every pending record currently in the live table.
|
|
84
|
+
* @param cap - how many resolved records may be kept.
|
|
85
|
+
* @returns the records to evict, oldest first (empty when within the cap).
|
|
86
|
+
*/
|
|
87
|
+
export declare function selectPendingOverflow(records: readonly PendingRecord[], cap?: number): PendingRecord[];
|
|
88
|
+
/**
|
|
89
|
+
* V27 G2.2: which review-state session rows a save evicts, as one pure rule
|
|
90
|
+
* both providers apply. The saving session is never a candidate (it is the most
|
|
91
|
+
* recent write by definition); the rest are ordered by their provider stamp
|
|
92
|
+
* ascending, with a missing stamp read as 0 = oldest, because an active session
|
|
93
|
+
* re-stamps its row on its next save and an unknown stamp is stale by
|
|
94
|
+
* construction. Exactly one row is dropped per over-cap save, which keeps the
|
|
95
|
+
* table at the cap in steady state.
|
|
96
|
+
*
|
|
97
|
+
* @param rows - the other sessions' rows, with their stamps.
|
|
98
|
+
* @param cap - how many session rows may exist.
|
|
99
|
+
* @returns the keys to delete, oldest first.
|
|
100
|
+
*/
|
|
101
|
+
export declare function selectSessionOverflow<T>(rows: readonly T[], options: {
|
|
102
|
+
keyOf(row: T): string;
|
|
103
|
+
stampOf(row: T): number;
|
|
104
|
+
}, cap?: number): string[];
|
|
63
105
|
/**
|
|
64
106
|
* Claim lifecycle (S3.3): pending →(claim)→ executing →(resolve)→ approved/rejected.
|
|
65
107
|
* release() rolls executing back to pending (failure path). A crash between
|
|
@@ -113,6 +155,13 @@ export interface EvolutionStateStorage {
|
|
|
113
155
|
* whole read → transform → write runs inside one provider transact, so a
|
|
114
156
|
* setPaused racing the run-core bookkeeping write can never interleave a
|
|
115
157
|
* stale load with a newer save.
|
|
158
|
+
*
|
|
159
|
+
* V27 S4: the record handed to `task` belongs to the task — a provider must
|
|
160
|
+
* NEVER pass the object it stores (both current providers hand out a copy:
|
|
161
|
+
* json re-parses its medium, the domain clones its record). Mutating the
|
|
162
|
+
* argument is not a supported way to write: a task that does so and then lets
|
|
163
|
+
* validation refuse the result would otherwise leave a mutated object in the
|
|
164
|
+
* provider's in-memory store.
|
|
116
165
|
*/
|
|
117
166
|
transactCuratorState(task: (current: CuratorStateRecord | null) => CuratorStateRecord | null): Promise<void>;
|
|
118
167
|
listPending(status?: PendingStatus): Promise<PendingRecord[]>;
|
|
@@ -139,6 +188,9 @@ export declare class EvolutionStateStorageRegistry extends Service {
|
|
|
139
188
|
private readonly providers;
|
|
140
189
|
/** C-7 (v18): per-name dispose, mirroring the evolution-io registry. */
|
|
141
190
|
private readonly disposals;
|
|
191
|
+
/** v28 G3.1 (STATE-04): the multi-provider warn fires once per ambiguous
|
|
192
|
+
* period, not once per state op (provider() runs on every read/write). */
|
|
193
|
+
private ambiguousWarned;
|
|
142
194
|
constructor(ctx: Context);
|
|
143
195
|
/** C-7 (v18): re-registering the IDENTICAL provider object is idempotent and
|
|
144
196
|
* returns the original dispose (HMR / re-mounted row); a DIFFERENT object
|