@mcp-abap-adt/auth-broker 4.0.0 → 5.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.
@@ -7,21 +7,50 @@
7
7
  *
8
8
  * **It carries names, never values.** `missingFields` holds store field names
9
9
  * (`authType`, `username`, `sncQop`) or broker option names
10
- * (`serviceKeyStore`, `provider`); the message is fixed wording plus the
11
- * destination's name and those names. A stored value — a password, a token,
12
- * even an `authType` that is not one of the four — never reaches it. There is
13
- * no `cause`: a provider's own error quotes the value it refused (`qop … got
14
- * '7'`), so it is not carried; its field names are mapped instead.
10
+ * (`serviceKeyStore`, `provider`, `renewal`); the message is fixed wording plus
11
+ * the destination's name and those names. A stored value — a password, a
12
+ * token, even an `authType` that is not one of the four — never reaches it.
13
+ *
14
+ * **When a provider or a strategy caused the refusal, its error is carried**
15
+ * as auth-errors read it (`readFailure`): `error` — kind, facts, the words
16
+ * auth-errors rendered and the diagnostics it admitted, nothing more. There is
17
+ * no `cause`: no message of any thrown value reaches the broker's error. The
18
+ * message then ends with `: <error.reason>`, the rendered words, never copied.
15
19
  */
20
+ import type { IAuthProviderError } from '@mcp-abap-adt/interfaces-auth';
16
21
  export declare class DestinationConfigError extends Error {
17
22
  readonly code: "DESTINATION_CONFIG";
18
23
  readonly destination: string;
19
24
  readonly missingFields: string[];
25
+ /**
26
+ * Present when the refusal was caused by a provider's or a strategy's
27
+ * failure: that error as auth-errors read it (`readFailure`), its
28
+ * diagnostics kept when it came from this copy of auth-errors. An own
29
+ * property only when present.
30
+ */
31
+ readonly error?: IAuthProviderError | undefined;
20
32
  /**
21
33
  * @param destination The destination's name, as the caller passed it.
22
34
  * @param missingFields Field or option names only.
23
35
  * @param reason Fixed wording: what is wrong, never a stored value.
36
+ * @param error The provider's or strategy's error, read by `readFailure`.
24
37
  */
25
- constructor(destination: string, missingFields: string[], reason: string);
38
+ constructor(destination: string, missingFields: string[], reason: string, error?: IAuthProviderError);
26
39
  }
40
+ /** What `isDestinationConfigError` vouches for: the fields, by structure. */
41
+ export interface DestinationConfigErrorLike {
42
+ readonly name: 'DestinationConfigError';
43
+ readonly code: 'DESTINATION_CONFIG';
44
+ readonly destination: string;
45
+ readonly missingFields: readonly string[];
46
+ }
47
+ /**
48
+ * Whether a value is a `DestinationConfigError` — or its JSON copy, or one
49
+ * of another installed copy of this package — by structure only: own data
50
+ * properties `name === 'DestinationConfigError'`, `code === 'DESTINATION_CONFIG'`,
51
+ * a string `destination` and an array of strings `missingFields`. No
52
+ * `instanceof`, no getter invoked; total — a Proxy whose traps throw answers
53
+ * false.
54
+ */
55
+ export declare function isDestinationConfigError(value: unknown): value is DestinationConfigErrorLike;
27
56
  //# sourceMappingURL=DestinationConfigError.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DestinationConfigError.d.ts","sourceRoot":"","sources":["../src/DestinationConfigError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAG,oBAAoB,CAAU;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAEjC;;;;OAIG;gBACS,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM;CASzE"}
1
+ {"version":3,"file":"DestinationConfigError.d.ts","sourceRoot":"","sources":["../src/DestinationConfigError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAExE,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAG,oBAAoB,CAAU;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IACjC;;;;;OAKG;IACH,SAAiB,KAAK,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAExD;;;;;OAKG;gBAED,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EAAE,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,kBAAkB;CAY7B;AAED,6EAA6E;AAC7E,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAUD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,0BAA0B,CAcrC"}
@@ -1,6 +1,4 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DestinationConfigError = void 0;
4
2
  /**
5
3
  * A destination that lacks what its type needs.
6
4
  *
@@ -10,12 +8,19 @@ exports.DestinationConfigError = void 0;
10
8
  *
11
9
  * **It carries names, never values.** `missingFields` holds store field names
12
10
  * (`authType`, `username`, `sncQop`) or broker option names
13
- * (`serviceKeyStore`, `provider`); the message is fixed wording plus the
14
- * destination's name and those names. A stored value — a password, a token,
15
- * even an `authType` that is not one of the four — never reaches it. There is
16
- * no `cause`: a provider's own error quotes the value it refused (`qop … got
17
- * '7'`), so it is not carried; its field names are mapped instead.
11
+ * (`serviceKeyStore`, `provider`, `renewal`); the message is fixed wording plus
12
+ * the destination's name and those names. A stored value — a password, a
13
+ * token, even an `authType` that is not one of the four — never reaches it.
14
+ *
15
+ * **When a provider or a strategy caused the refusal, its error is carried**
16
+ * as auth-errors read it (`readFailure`): `error` — kind, facts, the words
17
+ * auth-errors rendered and the diagnostics it admitted, nothing more. There is
18
+ * no `cause`: no message of any thrown value reaches the broker's error. The
19
+ * message then ends with `: <error.reason>`, the rendered words, never copied.
18
20
  */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.DestinationConfigError = void 0;
23
+ exports.isDestinationConfigError = isDestinationConfigError;
19
24
  class DestinationConfigError extends Error {
20
25
  code = 'DESTINATION_CONFIG';
21
26
  destination;
@@ -24,13 +29,50 @@ class DestinationConfigError extends Error {
24
29
  * @param destination The destination's name, as the caller passed it.
25
30
  * @param missingFields Field or option names only.
26
31
  * @param reason Fixed wording: what is wrong, never a stored value.
32
+ * @param error The provider's or strategy's error, read by `readFailure`.
27
33
  */
28
- constructor(destination, missingFields, reason) {
29
- super(`Destination "${destination}": ${reason} (${missingFields.join(', ')})`);
34
+ constructor(destination, missingFields, reason, error) {
35
+ const words = error === undefined ? reason : `${reason}: ${error.reason}`;
36
+ super(`Destination "${destination}": ${words} (${missingFields.join(', ')})`);
30
37
  this.name = 'DestinationConfigError';
31
38
  this.destination = destination;
32
39
  this.missingFields = [...missingFields];
40
+ if (error !== undefined)
41
+ this.error = error;
33
42
  Object.setPrototypeOf(this, DestinationConfigError.prototype);
34
43
  }
35
44
  }
36
45
  exports.DestinationConfigError = DestinationConfigError;
46
+ /** An own data property's value; `undefined` for an accessor or an absent key. */
47
+ function ownData(value, key) {
48
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
49
+ return descriptor !== undefined && 'value' in descriptor
50
+ ? descriptor.value
51
+ : undefined;
52
+ }
53
+ /**
54
+ * Whether a value is a `DestinationConfigError` — or its JSON copy, or one
55
+ * of another installed copy of this package — by structure only: own data
56
+ * properties `name === 'DestinationConfigError'`, `code === 'DESTINATION_CONFIG'`,
57
+ * a string `destination` and an array of strings `missingFields`. No
58
+ * `instanceof`, no getter invoked; total — a Proxy whose traps throw answers
59
+ * false.
60
+ */
61
+ function isDestinationConfigError(value) {
62
+ try {
63
+ if (typeof value !== 'object' || value === null)
64
+ return false;
65
+ if (ownData(value, 'name') !== 'DestinationConfigError')
66
+ return false;
67
+ if (ownData(value, 'code') !== 'DESTINATION_CONFIG')
68
+ return false;
69
+ if (typeof ownData(value, 'destination') !== 'string')
70
+ return false;
71
+ const fields = ownData(value, 'missingFields');
72
+ return (Array.isArray(fields) &&
73
+ fields.every((field) => typeof field === 'string'));
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
@@ -1,27 +1,53 @@
1
1
  /**
2
- * The broker's writes of a destination's session secret, retried until the
3
- * store takes them.
2
+ * The broker's writes of a destination's session secret: one plain queue per
3
+ * destination.
4
4
  *
5
- * A store failure is the storage's or a broker bug, never the token's, so it
6
- * does not fail the authentication: the result stays pending for its
7
- * destination and the writer tries again on its own — it does not wait for the
8
- * provider to be called again, since a request may be the process's last.
9
- *
10
- * - **One pending result per destination, the latest.** A newer result
11
- * replaces the one waiting; an older one is never written after it.
12
- * - **Attempts for one destination never overlap.** Each runs after the
13
- * previous one has settled.
14
- * - **A growing delay:** one second after the first failure, doubling, capped
15
- * at one minute; reset once a write lands. The timer is `unref()`ed, so a
16
- * pending write never keeps a process alive.
17
- * - **Failures are logged by class name** — never a message: the store holds
18
- * tokens, and its errors are foreign text.
19
- * - **`flush()`** gives every pending result one more attempt and rejects,
20
- * naming the destinations, if any is still not written.
5
+ * - **One at a time, in order.** The writes of one destination run one after
6
+ * another, in the order they were queued, each writing what it was given;
7
+ * an older write never runs after, and so never overwrites, a newer one.
8
+ * Destinations never wait on each other.
9
+ * - **Each write's own outcome.** A submission resolves with what its own
10
+ * write came to — landed, or the store's error — never with another
11
+ * submission's: no write is folded into another, so no caller is told
12
+ * "landed" for a write that never ran.
13
+ * - **A failed write stays pending.** The destination then has a pending
14
+ * write: the latest one that failed — every write is built from its
15
+ * build's logical state, so the latest is the one that must land. It is
16
+ * retried by the destination's next write, which carries the same state
17
+ * and, once it lands, clears it; or by `retry()` / `flush()`, which write
18
+ * the pending one again. **There is no retry timer:** nothing is retried on
19
+ * its own, and nothing is scheduled.
20
+ * - **The store's contract:** `saveSession` settles — resolves or rejects. A
21
+ * store that never settles holds its destination's queue; avoiding that is
22
+ * the consumer's. The writer builds no machinery against it.
23
+ * - **Failures are logged once each, with `logFields`** of auth-errors'
24
+ * classification (`persisting-tokens`) — never a message: the store holds
25
+ * tokens, and its errors are foreign text. Each failed attempt is one
26
+ * `warn` line, written here only: the row's persistence is given no logger.
27
+ * A retry that fails again is a new attempt, and its own line.
28
+ * - **`flush()`** retries every pending write once, after everything queued
29
+ * before it, and rejects naming the destinations still pending.
21
30
  */
31
+ import type { IAuthProviderError } from '@mcp-abap-adt/interfaces-auth';
22
32
  import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
23
- /** A thrown value's class, for a log line: never its message. */
24
- export declare function classLabel(error: unknown): string;
33
+ /** What one write came to: it landed, or the store's error. */
34
+ export type WriteOutcome = {
35
+ readonly landed: true;
36
+ } | {
37
+ readonly landed: false;
38
+ readonly error: unknown;
39
+ };
40
+ /**
41
+ * One destination whose session write still fails, in `flush()`'s
42
+ * `AggregateError`: the destination and the store's error as auth-errors
43
+ * classifies it (`persisting-tokens`) — never the store's message.
44
+ */
45
+ export declare class SessionWriteFailure extends Error {
46
+ readonly destination: string;
47
+ /** `classify(storeError, 'persisting-tokens')`. */
48
+ readonly error: IAuthProviderError;
49
+ constructor(destination: string, storeError: unknown);
50
+ }
25
51
  /** `T`: what one write takes — the result, with what the broker writes beside it. */
26
52
  export declare class SessionWriter<T> {
27
53
  private readonly write;
@@ -30,25 +56,36 @@ export declare class SessionWriter<T> {
30
56
  /**
31
57
  * @param write Writes one result for a destination; throws when the store
32
58
  * does not take it.
59
+ * @param logger The broker's quiet logger (`quietLogger`): a line never
60
+ * throws and never rejects, so what a write came to is the store's answer
61
+ * alone, and `submit` / `retry` never reject.
33
62
  */
34
63
  constructor(write: (destination: string, result: T) => Promise<void>, logger: ILogger);
35
64
  /**
36
- * Take a new result for the destination and try to write it now. Never
37
- * throws: resolves once this attempt has settled, written or left pending.
65
+ * Queue `result` for the destination, after every write queued before it.
66
+ * Never rejects: resolves once this write has settled, with what it came
67
+ * to. Landed: the destination's pending write, if any — an older one — is
68
+ * replaced. Failed: this write is the destination's pending one.
69
+ */
70
+ submit(destination: string, result: T): Promise<WriteOutcome>;
71
+ /**
72
+ * Wait for every write of the destination queued before this call, then
73
+ * write its pending write once more, if one is left. Never rejects:
74
+ * resolves landed when, by its turn, nothing is pending — every write
75
+ * queued before it landed, or the latest of them did, replacing a failed
76
+ * older one — else with the attempt's own outcome. It writes the pending
77
+ * write as its turn finds it: the latest that failed.
38
78
  */
39
- submit(destination: string, result: T): Promise<void>;
79
+ retry(destination: string): Promise<WriteOutcome>;
40
80
  /**
41
- * One more attempt for every pending result; resolves when all are written,
42
- * rejects naming the destinations still pending — each failure as its
43
- * destination and its error's class only, never the store's message. The
44
- * retries go on after a rejection.
81
+ * One more attempt for every pending write, each after everything queued
82
+ * for its destination before it; resolves when all landed, rejects naming
83
+ * the destinations still pending — one `SessionWriteFailure` per
84
+ * destination, never the store's message. What still fails stays pending.
45
85
  */
46
86
  flush(): Promise<void>;
47
- private queueOf;
48
- /** Runs after the destination's previous attempt; never rejects. */
49
- private attempt;
50
- private writePending;
51
- private schedule;
52
- private cancelTimer;
87
+ /** Runs `job` after the destination's last queued write has settled. */
88
+ private enqueue;
89
+ private logFailure;
53
90
  }
54
91
  //# sourceMappingURL=SessionWriter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SessionWriter.d.ts","sourceRoot":"","sources":["../src/SessionWriter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAgB9D,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKjD;AAED,qFAAqF;AACrF,qBAAa,aAAa,CAAC,CAAC;IAQxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IARzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IAEtD;;;OAGG;gBAEgB,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,EACxD,MAAM,EAAE,OAAO;IAGlC;;;OAGG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3D;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB5B,OAAO,CAAC,OAAO;IASf,oEAAoE;IACpE,OAAO,CAAC,OAAO;YAMD,YAAY;IAgC1B,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,WAAW;CAMpB"}
1
+ {"version":3,"file":"SessionWriter.d.ts","sourceRoot":"","sources":["../src/SessionWriter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACxE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAe9D,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAIxD;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;gBAEvB,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO;CAQrD;AAED,qFAAqF;AACrF,qBAAa,aAAa,CAAC,CAAC;IAWxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAXzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IAEtD;;;;;;OAMG;gBAEgB,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,EACxD,MAAM,EAAE,OAAO;IAGlC;;;;;OAKG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC;IAgB7D;;;;;;;OAOG;IACH,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAiBjD;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB5B,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,UAAU;CAMnB"}
@@ -1,37 +1,57 @@
1
1
  "use strict";
2
2
  /**
3
- * The broker's writes of a destination's session secret, retried until the
4
- * store takes them.
3
+ * The broker's writes of a destination's session secret: one plain queue per
4
+ * destination.
5
5
  *
6
- * A store failure is the storage's or a broker bug, never the token's, so it
7
- * does not fail the authentication: the result stays pending for its
8
- * destination and the writer tries again on its own — it does not wait for the
9
- * provider to be called again, since a request may be the process's last.
10
- *
11
- * - **One pending result per destination, the latest.** A newer result
12
- * replaces the one waiting; an older one is never written after it.
13
- * - **Attempts for one destination never overlap.** Each runs after the
14
- * previous one has settled.
15
- * - **A growing delay:** one second after the first failure, doubling, capped
16
- * at one minute; reset once a write lands. The timer is `unref()`ed, so a
17
- * pending write never keeps a process alive.
18
- * - **Failures are logged by class name** — never a message: the store holds
19
- * tokens, and its errors are foreign text.
20
- * - **`flush()`** gives every pending result one more attempt and rejects,
21
- * naming the destinations, if any is still not written.
6
+ * - **One at a time, in order.** The writes of one destination run one after
7
+ * another, in the order they were queued, each writing what it was given;
8
+ * an older write never runs after, and so never overwrites, a newer one.
9
+ * Destinations never wait on each other.
10
+ * - **Each write's own outcome.** A submission resolves with what its own
11
+ * write came to — landed, or the store's error — never with another
12
+ * submission's: no write is folded into another, so no caller is told
13
+ * "landed" for a write that never ran.
14
+ * - **A failed write stays pending.** The destination then has a pending
15
+ * write: the latest one that failed — every write is built from its
16
+ * build's logical state, so the latest is the one that must land. It is
17
+ * retried by the destination's next write, which carries the same state
18
+ * and, once it lands, clears it; or by `retry()` / `flush()`, which write
19
+ * the pending one again. **There is no retry timer:** nothing is retried on
20
+ * its own, and nothing is scheduled.
21
+ * - **The store's contract:** `saveSession` settles — resolves or rejects. A
22
+ * store that never settles holds its destination's queue; avoiding that is
23
+ * the consumer's. The writer builds no machinery against it.
24
+ * - **Failures are logged once each, with `logFields`** of auth-errors'
25
+ * classification (`persisting-tokens`) — never a message: the store holds
26
+ * tokens, and its errors are foreign text. Each failed attempt is one
27
+ * `warn` line, written here only: the row's persistence is given no logger.
28
+ * A retry that fails again is a new attempt, and its own line.
29
+ * - **`flush()`** retries every pending write once, after everything queued
30
+ * before it, and rejects naming the destinations still pending.
22
31
  */
23
32
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.SessionWriter = void 0;
25
- exports.classLabel = classLabel;
26
- const FIRST_DELAY_MS = 1_000;
27
- const MAX_DELAY_MS = 60_000;
28
- /** A thrown value's class, for a log line: never its message. */
29
- function classLabel(error) {
30
- if (error instanceof Error) {
31
- return error.constructor?.name || 'Error';
33
+ exports.SessionWriter = exports.SessionWriteFailure = void 0;
34
+ const auth_errors_1 = require("@mcp-abap-adt/auth-errors");
35
+ const LANDED = Object.freeze({ landed: true });
36
+ /**
37
+ * One destination whose session write still fails, in `flush()`'s
38
+ * `AggregateError`: the destination and the store's error as auth-errors
39
+ * classifies it (`persisting-tokens`) — never the store's message.
40
+ */
41
+ class SessionWriteFailure extends Error {
42
+ destination;
43
+ /** `classify(storeError, 'persisting-tokens')`. */
44
+ error;
45
+ constructor(destination, storeError) {
46
+ const error = (0, auth_errors_1.classify)(storeError, 'persisting-tokens');
47
+ super(`"${destination}": ${error.reason}`);
48
+ this.name = 'SessionWriteFailure';
49
+ this.destination = destination;
50
+ this.error = error;
51
+ Object.setPrototypeOf(this, SessionWriteFailure.prototype);
32
52
  }
33
- return typeof error;
34
53
  }
54
+ exports.SessionWriteFailure = SessionWriteFailure;
35
55
  /** `T`: what one write takes — the result, with what the broker writes beside it. */
36
56
  class SessionWriter {
37
57
  write;
@@ -40,96 +60,100 @@ class SessionWriter {
40
60
  /**
41
61
  * @param write Writes one result for a destination; throws when the store
42
62
  * does not take it.
63
+ * @param logger The broker's quiet logger (`quietLogger`): a line never
64
+ * throws and never rejects, so what a write came to is the store's answer
65
+ * alone, and `submit` / `retry` never reject.
43
66
  */
44
67
  constructor(write, logger) {
45
68
  this.write = write;
46
69
  this.logger = logger;
47
70
  }
48
71
  /**
49
- * Take a new result for the destination and try to write it now. Never
50
- * throws: resolves once this attempt has settled, written or left pending.
72
+ * Queue `result` for the destination, after every write queued before it.
73
+ * Never rejects: resolves once this write has settled, with what it came
74
+ * to. Landed: the destination's pending write, if any — an older one — is
75
+ * replaced. Failed: this write is the destination's pending one.
51
76
  */
52
- async submit(destination, result) {
53
- const queue = this.queueOf(destination);
54
- queue.pending = result;
55
- this.cancelTimer(queue);
56
- await this.attempt(destination, queue);
77
+ submit(destination, result) {
78
+ return this.enqueue(destination, async (queue) => {
79
+ try {
80
+ await this.write(destination, result);
81
+ }
82
+ catch (error) {
83
+ queue.pending = { write: result, error };
84
+ this.logFailure(destination, error);
85
+ return Object.freeze({ landed: false, error });
86
+ }
87
+ // Every write queued before this one has settled: a pending write is
88
+ // older, and this one carries the state it was built from.
89
+ queue.pending = undefined;
90
+ return LANDED;
91
+ });
57
92
  }
58
93
  /**
59
- * One more attempt for every pending result; resolves when all are written,
60
- * rejects naming the destinations still pending — each failure as its
61
- * destination and its error's class only, never the store's message. The
62
- * retries go on after a rejection.
94
+ * Wait for every write of the destination queued before this call, then
95
+ * write its pending write once more, if one is left. Never rejects:
96
+ * resolves landed when, by its turn, nothing is pending — every write
97
+ * queued before it landed, or the latest of them did, replacing a failed
98
+ * older one — else with the attempt's own outcome. It writes the pending
99
+ * write as its turn finds it: the latest that failed.
100
+ */
101
+ retry(destination) {
102
+ return this.enqueue(destination, async (queue) => {
103
+ const pending = queue.pending;
104
+ if (pending === undefined)
105
+ return LANDED;
106
+ try {
107
+ await this.write(destination, pending.write);
108
+ }
109
+ catch (error) {
110
+ // Nothing else ran meanwhile: the queue runs one write at a time.
111
+ pending.error = error;
112
+ this.logFailure(destination, error);
113
+ return Object.freeze({ landed: false, error });
114
+ }
115
+ queue.pending = undefined;
116
+ return LANDED;
117
+ });
118
+ }
119
+ /**
120
+ * One more attempt for every pending write, each after everything queued
121
+ * for its destination before it; resolves when all landed, rejects naming
122
+ * the destinations still pending — one `SessionWriteFailure` per
123
+ * destination, never the store's message. What still fails stays pending.
63
124
  */
64
125
  async flush() {
65
- const queues = [...this.queues];
66
- await Promise.all(queues.map(([destination, queue]) => {
67
- if (queue.pending === undefined)
68
- return queue.tail;
69
- this.cancelTimer(queue);
70
- return this.attempt(destination, queue);
71
- }));
72
- const failed = queues.filter(([, queue]) => queue.pending !== undefined);
126
+ const destinations = [...this.queues.keys()];
127
+ await Promise.all(destinations.map((d) => this.retry(d)));
128
+ const failed = [];
129
+ for (const destination of destinations) {
130
+ const pending = this.queues.get(destination)?.pending;
131
+ if (pending !== undefined)
132
+ failed.push([destination, pending]);
133
+ }
73
134
  if (failed.length > 0) {
74
135
  // The stores' own errors are not carried: their messages, causes and
75
136
  // properties are foreign text that may quote what was being written.
76
- throw new AggregateError(failed.map(([destination, queue]) => new Error(`"${destination}": ${classLabel(queue.lastError)}`)), `Session writes still failing for ${failed
137
+ throw new AggregateError(failed.map(([destination, pending]) => new SessionWriteFailure(destination, pending.error)), `Session writes still failing for ${failed
77
138
  .map(([destination]) => `"${destination}"`)
78
- .join(', ')}; the broker keeps retrying them`);
139
+ .join(', ')}; each stays pending until its destination's next write or flush()`);
79
140
  }
80
141
  }
81
- queueOf(destination) {
142
+ /** Runs `job` after the destination's last queued write has settled. */
143
+ enqueue(destination, job) {
82
144
  let queue = this.queues.get(destination);
83
145
  if (!queue) {
84
- queue = { tail: Promise.resolve(), failures: 0 };
146
+ queue = { tail: Promise.resolve() };
85
147
  this.queues.set(destination, queue);
86
148
  }
87
- return queue;
88
- }
89
- /** Runs after the destination's previous attempt; never rejects. */
90
- attempt(destination, queue) {
91
- const run = queue.tail.then(() => this.writePending(destination, queue));
92
- queue.tail = run;
149
+ const own = queue;
150
+ // `job` never rejects; the tail is chained whatever the previous came to.
151
+ const run = own.tail.then(() => job(own), () => job(own));
152
+ own.tail = run;
93
153
  return run;
94
154
  }
95
- async writePending(destination, queue) {
96
- const result = queue.pending;
97
- if (result === undefined)
98
- return;
99
- try {
100
- await this.write(destination, result);
101
- }
102
- catch (error) {
103
- queue.failures += 1;
104
- queue.lastError = error;
105
- const delay = Math.min(FIRST_DELAY_MS * 2 ** (queue.failures - 1), MAX_DELAY_MS);
106
- this.logger.warn(`[AuthBroker] Session write for ${destination} failed; the token stands, the write is retried in ${delay} ms`, { error: classLabel(error), attempt: queue.failures });
107
- this.schedule(destination, queue, delay);
108
- return;
109
- }
110
- queue.failures = 0;
111
- queue.lastError = undefined;
112
- // A newer result that arrived meanwhile stays pending: its own attempt is
113
- // queued after this one.
114
- if (queue.pending === result) {
115
- queue.pending = undefined;
116
- this.cancelTimer(queue);
117
- }
118
- }
119
- schedule(destination, queue, delay) {
120
- this.cancelTimer(queue);
121
- const timer = setTimeout(() => {
122
- queue.timer = undefined;
123
- void this.attempt(destination, queue);
124
- }, delay);
125
- timer.unref();
126
- queue.timer = timer;
127
- }
128
- cancelTimer(queue) {
129
- if (queue.timer) {
130
- clearTimeout(queue.timer);
131
- queue.timer = undefined;
132
- }
155
+ logFailure(destination, error) {
156
+ this.logger.warn(`[AuthBroker] Session write for ${destination} failed; it stays pending until the destination's next write or flush()`, (0, auth_errors_1.logFields)((0, auth_errors_1.classify)(error, 'persisting-tokens')));
133
157
  }
134
158
  }
135
159
  exports.SessionWriter = SessionWriter;