@ultimat3/core 27.7.0 → 27.8.1
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/README.md +1 -1
- package/package.json +2 -2
- package/src/index.ts +3 -0
- package/src/outage-log.ts +53 -0
- package/src/single-flight.ts +18 -0
package/README.md
CHANGED
|
@@ -731,7 +731,7 @@ nothing consulted it before deciding to try again. `As of 2026-08-23`.
|
|
|
731
731
|
| Export | The one answer | The question it settles |
|
|
732
732
|
|---|---|---|
|
|
733
733
|
| `backoffDelay({ attempt, base, max, factor?, curve?, jitter?, random? })` | one curve — `exponential \| linear \| fixed`, `full \| equal \| none` — 1-based `attempt`, clamped to `max` **before** jitter, rounded, and `0` rather than `NaN` | how long to wait. `random` is injectable, so a schedule is a unit test rather than a range |
|
|
734
|
-
| `singleFlight({ deadlineMs?, schedule? })` → `run(key, work, join?)`, `size` | N callers on one key are ONE run | who pays for a miss. Eviction is identity-checked, so a load that settles late never drops the load that replaced it; `deadlineMs` frees the KEY a wedged load would hold forever — it never cancels the work and never rejects a joiner |
|
|
734
|
+
| `singleFlight({ deadlineMs?, schedule? })` → `run(key, work, join?)`, `size`, `keys()`, `evict(key)` | N callers on one key are ONE run | who pays for a miss. Eviction is identity-checked, so a load that settles late never drops the load that replaced it; `deadlineMs` frees the KEY a wedged load would hold forever — it never cancels the work and never rejects a joiner. `evict(key)` does the same on demand, for a caller that learns the load in flight began before a write its result must reflect |
|
|
735
735
|
| `flightGate({ maxConcurrent, maxQueued }, { subject?, overflow? })` | one bound, one queue, one refusal | how many at once. Past the queue the answer is `X_FLIGHT_GATE_OVERLOADED` (503) and never a longer queue; the slot is HANDED to a waiter, never released and re-acquired. Both limits are screened at construction (`finiteCount`, 0 allowed); a width of 0 refuses every caller rather than queueing for a slot that never frees. `run(work, signal?)`: an abort while QUEUED removes the waiter and rejects with the abort's reason |
|
|
736
736
|
| `generationFence(subject)` → `generation()`, `bump()`, `guard(issued)` | whether an answer still applies | `X_SUPERSEDED` (499) and `isSuperseded(error)` — the piece nothing in the tree had. `guard` compares `!==`, never `<` |
|
|
737
737
|
| `isRetryableStatus(status)`, `RETRYABLE_STATUSES` | `>= 500`, plus 408, 409, 425, 429 | which HTTP answers are worth repeating |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/core",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.8.1",
|
|
4
4
|
"description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ultimat3/schema": "27.
|
|
40
|
+
"@ultimat3/schema": "27.8.1"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/index.ts
CHANGED
|
@@ -679,6 +679,9 @@ export {
|
|
|
679
679
|
resetMeasurementActor,
|
|
680
680
|
} from './measurement-actor';
|
|
681
681
|
export { nearestName } from './nearest-name';
|
|
682
|
+
// An outage said on failures 1, 2, 4, 8, … and once when it ends.
|
|
683
|
+
export type { OutageLog, OutageLogOptions } from './outage-log';
|
|
684
|
+
export { isOutageMilestone, outageLog } from './outage-log';
|
|
682
685
|
/** The message pwa's `sw.js` posts and realtime's outbox listens for. */
|
|
683
686
|
export { OUTBOX_DRAIN_MESSAGE, type OutboxDrainMessage } from './outbox-drain';
|
|
684
687
|
/** The `<meta name>`s render writes and the page client, realtime and pwa read. */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Single responsibility: one condition that KEEPS failing, said rarely — on failures 1, 2, 4, 8, …
|
|
2
|
+
// and once more when it ends. An outage is one fact; a line per retry per pod for as long as it
|
|
3
|
+
// lasts is the fact buried under itself.
|
|
4
|
+
|
|
5
|
+
import { type LogFields, logger } from './logger';
|
|
6
|
+
|
|
7
|
+
export interface OutageLogOptions {
|
|
8
|
+
/** The line's message while the condition fails. Low cardinality: it is what a monitor groups on. */
|
|
9
|
+
readonly event: string;
|
|
10
|
+
/**
|
|
11
|
+
* `'warn'` for a process that is degraded and still serving (a publisher without its bus).
|
|
12
|
+
* `'error'` for one that cannot do its job (a `sync` node without its bus).
|
|
13
|
+
*/
|
|
14
|
+
readonly level: 'warn' | 'error';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OutageLog {
|
|
18
|
+
/**
|
|
19
|
+
* One more failure. Written — with `failures`, the count since it last worked — on the 1st,
|
|
20
|
+
* 2nd, 4th, 8th, …; answers whether this one was, so a caller thins a second sink the same way.
|
|
21
|
+
*/
|
|
22
|
+
failed(fields?: LogFields): boolean;
|
|
23
|
+
/** It works again: ONE `info` line, `<event> recovered`, with `after`. Silent when nothing had failed. */
|
|
24
|
+
recovered(fields?: LogFields): void;
|
|
25
|
+
/** Failures since it last worked. */
|
|
26
|
+
readonly failures: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True on 1, 2, 4, 8, …: the attempts an outage is said on. */
|
|
30
|
+
export function isOutageMilestone(failures: number): boolean {
|
|
31
|
+
return failures > 0 && (failures & (failures - 1)) === 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function outageLog(options: OutageLogOptions): OutageLog {
|
|
35
|
+
let failures = 0;
|
|
36
|
+
return {
|
|
37
|
+
get failures(): number {
|
|
38
|
+
return failures;
|
|
39
|
+
},
|
|
40
|
+
failed(fields?: LogFields): boolean {
|
|
41
|
+
failures += 1;
|
|
42
|
+
if (!isOutageMilestone(failures)) return false;
|
|
43
|
+
logger[options.level](options.event, { ...fields, failures });
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
recovered(fields?: LogFields): void {
|
|
47
|
+
if (failures === 0) return;
|
|
48
|
+
const after = failures;
|
|
49
|
+
failures = 0;
|
|
50
|
+
logger.info(`${options.event} recovered`, { ...fields, after });
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/single-flight.ts
CHANGED
|
@@ -40,6 +40,15 @@ export interface SingleFlight {
|
|
|
40
40
|
): Promise<T>;
|
|
41
41
|
/** In-flight loads right now. A number that does not fall back to `0` is a leak. */
|
|
42
42
|
readonly size: number;
|
|
43
|
+
/** The keys held by a load right now — what a caller that must `evict` by a rule walks. */
|
|
44
|
+
keys(): readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* Free `key` NOW, so the next `run` leads a fresh load instead of joining the one in flight.
|
|
47
|
+
* For a caller that learns the load in flight is already wrong — it began before a write its
|
|
48
|
+
* result must reflect. The load is not cancelled and its own callers still get its value; only
|
|
49
|
+
* later ones stop joining it. `false` when nothing held the key.
|
|
50
|
+
*/
|
|
51
|
+
evict(key: string): boolean;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
/** The leader's promise, the box its merged context lives in, and its deadline's canceller. */
|
|
@@ -73,6 +82,15 @@ export function singleFlight(options?: SingleFlightOptions): SingleFlight {
|
|
|
73
82
|
return inflight.size;
|
|
74
83
|
},
|
|
75
84
|
|
|
85
|
+
keys: () => [...inflight.keys()],
|
|
86
|
+
|
|
87
|
+
evict(key: string): boolean {
|
|
88
|
+
const held = inflight.get(key);
|
|
89
|
+
if (held === undefined) return false;
|
|
90
|
+
held.cancelDeadline();
|
|
91
|
+
return inflight.delete(key);
|
|
92
|
+
},
|
|
93
|
+
|
|
76
94
|
run<T, C = undefined>(
|
|
77
95
|
key: string,
|
|
78
96
|
work: (shared: () => C | undefined) => Promise<T>,
|