fix(credentials-local): one operation chain, read-modify-write under the shared writer lock, and a quote-aware line editor
Review round three, credentials half. dsh-atomic-write grows the cross-process writer-lock primitive (withFileLock: wx sentinel, bounded backoff, stale takeover via onStaleBreak, deadline failure) plus a dirMode option, and settings-local migrates its private copy to it; both providers now create harness-home directories 0700. credentials-local reuses the reviewed settings-local shape: watcher reloads and line edits share one settled operation chain; every write re-reads the document under the lock and publishes unobserved external entries before editing, so an edit inside the debounce window (or another process's write) can never be overwritten; the watcher's ready signal queues one reconcile closing the startup gap. The line editor is now physical-line aware: continuation lines of a quoted multi-line value are never mistaken for assignments, untouched lines keep their exact bytes (CRLF included), an edited line keeps its own terminator, and appends use the document's dominant ending. A multi-line entry reports writable: false, matching what set() would do. The Credentials base class owns a contained notifyUpdated fan-out: providers publish only after the commit, every listener runs, sync throws and async rejections are logged without failing the committed write, and INVARIANT-coded failures rethrow after the fan-out.
This commit is contained in:
@@ -55,7 +55,12 @@ declare module 'cordis' {
|
||||
/**
|
||||
* Committed change to a provider-managed credential source: a `set`, an
|
||||
* `unset`, or an external edit observed in storage. Ambient
|
||||
* process-environment changes are not observable and never emit.
|
||||
* process-environment changes are not observable and never emit. Listener
|
||||
* failures are contained and logged — a sync throw and an async rejection
|
||||
* alike — without changing the committed operation's outcome, except
|
||||
* `INVARIANT`-coded failures, which rethrow after every listener ran;
|
||||
* that rethrow reaches the emitter only from synchronous listeners, so
|
||||
* invariant checks on this event must not be async functions.
|
||||
* @param ref - the reference whose stored value changed.
|
||||
* @mode emit
|
||||
*/
|
||||
@@ -109,6 +114,49 @@ export abstract class Credentials extends Service {
|
||||
* @param ref - the reference to remove.
|
||||
*/
|
||||
abstract unset(ref: CredentialRef): Promise<void>
|
||||
|
||||
/* jscpd:ignore-start -- deliberate symmetry with the settings seam's commit
|
||||
fan-out: the contained-dispatch shape is the reviewed listener-lifecycle
|
||||
contract, and extracting it would couple the two seams' event semantics. */
|
||||
/**
|
||||
* Fan `credentials/updated` out with contained listener failures: every
|
||||
* listener runs, and a sync throw or async rejection is logged without
|
||||
* changing the committed operation's outcome — except `INVARIANT`-coded
|
||||
* failures, which rethrow after every listener ran (the rethrow reaches the
|
||||
* caller only from synchronous listeners, so invariant checks on this event
|
||||
* must not be async functions). Providers call this only after the write or
|
||||
* reload actually committed, so a broken observer can never make a durable
|
||||
* change look failed.
|
||||
* @param ref - the reference whose stored value changed.
|
||||
*/
|
||||
protected notifyUpdated(ref: CredentialRef): void {
|
||||
let invariantFailure: unknown
|
||||
const args = ['credentials/updated', ref]
|
||||
for (const listener of this.ctx.events.dispatch('emit', args) as Array<(...listenerArgs: unknown[]) => unknown>) {
|
||||
try {
|
||||
const returned = listener(ref)
|
||||
if (returned != null && typeof (returned as PromiseLike<unknown>).then === 'function') {
|
||||
void Promise.resolve(returned as PromiseLike<unknown>).then(undefined, (error: unknown) => {
|
||||
this.warnListenerFailure(ref, error)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as { code?: unknown } | null)?.code === 'INVARIANT') {
|
||||
invariantFailure ??= error
|
||||
continue
|
||||
}
|
||||
this.warnListenerFailure(ref, error)
|
||||
}
|
||||
}
|
||||
if (invariantFailure !== undefined) throw invariantFailure as Error
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
/** Contained-listener diagnostic shared by the sync and async failure paths. */
|
||||
private warnListenerFailure(ref: CredentialRef, error: unknown): void {
|
||||
this.ctx.logger.warn('credentials: a credentials/updated listener for "%s" failed', ref)
|
||||
this.ctx.logger.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
export default Credentials
|
||||
|
||||
Reference in New Issue
Block a user