@ultimat3/core 27.10.1 → 27.10.2
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/CLAUDE.md +2 -2
- package/README.md +1 -0
- package/package.json +2 -2
- package/src/client-dispatch.ts +8 -1
- package/src/client-transport.ts +1 -1
- package/src/error-reporter.ts +5 -4
- package/src/error-stack.ts +28 -0
- package/src/exports/error-contract.ts +1 -0
- package/src/index.ts +1 -0
package/CLAUDE.md
CHANGED
|
@@ -6,14 +6,14 @@ change to every package.
|
|
|
6
6
|
|
|
7
7
|
| Rule | |
|
|
8
8
|
|---|---|
|
|
9
|
-
| Deps | `@ultimat3/schema` only (`bun-types
|
|
9
|
+
| Deps | `@ultimat3/schema` only (`bun-types`: types) |
|
|
10
10
|
| Errors | subclass `UltimateError`; never `throw new Error` |
|
|
11
11
|
| Values in a message | `renderCauseValue()` / `renderFixLiteral()`; never raw `JSON.stringify`, `String()` or `${…}` on an `unknown` |
|
|
12
12
|
| A value in a `fix:` a shell READS | `renderFixShellArg(value, placeholder)` — `renderFixLiteral`'s double quotes leave `$(…)`, `` ` `` and `${…}` live. An ordinary path or URL passes through; anything a shell would read becomes the placeholder |
|
|
13
13
|
| Whether that value TRAVELS | `isFixShellSafe(value)`, the predicate `renderFixShellArg` is built on. A placeholder is not a runnable command, so a `fix:` whose value sits mid-command asks this first and emits PROSE when the answer is no (`@ultimat3/auth`'s `jwks.ts`, `@ultimat3/scraping`'s `profileLocked`) |
|
|
14
14
|
| Rendering the 3-line format | nothing to remember — `UltimateError`'s CONSTRUCTOR escapes `code`, `title`, `cause`, `fix` and `docs` with `singleLine()`. Call it yourself only for a shape this class never built (a `Finding`) |
|
|
15
15
|
| A value a CALLER supplied | `describeValue()` — shape, never content. `renderCauseValue` is safe against throwing, not against leaking |
|
|
16
|
-
| Reading a caught value | `renderThrowable()` / `isThrownError()` / `stringField()`; never `error.message`, `instanceof Error` or `typeof error.code === 'string'` |
|
|
16
|
+
| Reading a caught value | `renderThrowable()` / `isThrownError()` / `stringField()` / `failureStack()`; never `error.message`, `instanceof Error` or `typeof error.code === 'string'` |
|
|
17
17
|
| New code | add to `CORE_CODE_TITLES` in `core-error-codes.ts` — a side-effect anchor the barrel bare-imports, so `UltimateError` alone never carries the table |
|
|
18
18
|
| Where an error points | `ERROR_DOCS_URL` — one constant, never a per-code URL. `docs:` is omitted at construction and resolved from the registry |
|
|
19
19
|
| Time | take a `Clock`; `Date.now()` / `new Date()` only inside `clock.ts` |
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Zero dependencies, zero `@ultimat3/*` imports.
|
|
|
7
7
|
|---|---|
|
|
8
8
|
| `UltimateError`, the 3-line rendering, `--json` shape | `errors.ts` |
|
|
9
9
|
| rendering an app's value into a `cause` / `fix` without throwing | `error-render.ts` |
|
|
10
|
+
| a thrown value's stack, its `name: message` line restored when Bun dropped it — `failureStack` | `error-stack.ts` |
|
|
10
11
|
| code → `{ title, docs }` registry, `registerErrorCodes()` | `error-codes.ts` |
|
|
11
12
|
| the one lazy `AsyncLocalStorage`, every ambient scope in the framework | `async-context.ts` |
|
|
12
13
|
| request context on that seam | `context.ts` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/core",
|
|
3
|
-
"version": "27.10.
|
|
3
|
+
"version": "27.10.2",
|
|
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.10.
|
|
40
|
+
"@ultimat3/schema": "27.10.2"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/client-dispatch.ts
CHANGED
|
@@ -50,11 +50,18 @@ export interface TransportRequest {
|
|
|
50
50
|
/** This call's retry override, handed to the `flight`. */
|
|
51
51
|
readonly retry?: ClientRetry | undefined;
|
|
52
52
|
/**
|
|
53
|
-
* The decoded records envelope, after its records were adopted
|
|
53
|
+
* The decoded records envelope, after its records were adopted (with `adopt: false`, they are
|
|
54
|
+
* not; the caller adopts them) — only when the answer carried
|
|
54
55
|
* `x-ultimate-records: 1` and belongs to the current principal. What a caller reads to know the
|
|
55
56
|
* ORDER of `records[type]`, which the store (keyed, unordered) cannot give it back.
|
|
56
57
|
*/
|
|
57
58
|
readonly onEnvelope?: ((envelope: RecordEnvelope) => void) | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* `false`: the records are NOT adopted here — the caller adopts them from `onEnvelope`, inside a
|
|
61
|
+
* notification of its own. For a read whose rows must land together with something only the
|
|
62
|
+
* caller holds: a channel's catch-up and the frames held behind it (`@ultimat3/realtime`).
|
|
63
|
+
*/
|
|
64
|
+
readonly adopt?: boolean | undefined;
|
|
58
65
|
/**
|
|
59
66
|
* How a 2xx body is read. `'json'` (the default) decodes it and adopts a records envelope;
|
|
60
67
|
* `'text'` answers the body as a string, untouched — an HTML fragment, a CSV — sends
|
package/src/client-transport.ts
CHANGED
|
@@ -76,7 +76,7 @@ export async function clientTransport<T = unknown>(req: TransportRequest): Promi
|
|
|
76
76
|
// A write that crossed a rescope HAS landed, so its caller is told — but its rows are the
|
|
77
77
|
// previous principal's, and the new scope's store never sees them.
|
|
78
78
|
if (current === issued) {
|
|
79
|
-
adopt(envelope);
|
|
79
|
+
if (req.adopt !== false) adopt(envelope);
|
|
80
80
|
if (answer.enveloped) req.onEnvelope?.(envelope);
|
|
81
81
|
}
|
|
82
82
|
return envelope.data as T;
|
package/src/error-reporter.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { type Clock, systemClock } from './clock';
|
|
7
7
|
import { tryUseContext } from './context';
|
|
8
8
|
import { DEFAULT_ENVIRONMENT, type Environment, tryResolveEnvironment } from './environment';
|
|
9
|
-
import { renderThrowable
|
|
9
|
+
import { renderThrowable } from './error-render';
|
|
10
|
+
import { failureStack } from './error-stack';
|
|
10
11
|
import { toUltimateError } from './errors';
|
|
11
12
|
import { logger } from './logger';
|
|
12
13
|
import type { Role } from './roles';
|
|
@@ -181,9 +182,9 @@ export function errorReport(error: unknown, options: ReportErrorOptions): ErrorR
|
|
|
181
182
|
meta: normalized.meta,
|
|
182
183
|
// The thrown value's own stack, not the wrapper's: `toUltimateError` builds its `InternalError`
|
|
183
184
|
// at this line, so the wrapper's stack points here rather than at the throw. Read through
|
|
184
|
-
// `
|
|
185
|
-
// on a caught value — and
|
|
186
|
-
stack:
|
|
185
|
+
// `failureStack` (total, and its header restored when Bun dropped it), because `.stack` is a
|
|
186
|
+
// property operation on a caught value — and reporting never throws.
|
|
187
|
+
stack: failureStack(error) ?? normalized.stack,
|
|
187
188
|
resource: serviceResource(),
|
|
188
189
|
environment: environmentNow(),
|
|
189
190
|
release: release ?? ctx?.buildId ?? null,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The ONE reader of a thrown value's stack: `.stack`, opening with its own `name: message` line.
|
|
2
|
+
// Bun 1.4 renders `.stack` lazily, and a full collection between the throw crossing an `await`
|
|
3
|
+
// and the first read leaves it opening with a bare `Error` (#773) — the frames survive, the
|
|
4
|
+
// header does not. The header is recomposed from the fields that did survive. Total: never throws.
|
|
5
|
+
|
|
6
|
+
import { stringField } from './error-render';
|
|
7
|
+
|
|
8
|
+
/** A stack's first line when it is a frame and not a header (` at f (file:1:2)`). */
|
|
9
|
+
const FRAME_LINE = /^\s+at\s/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `value.stack`, with the header restored when the runtime dropped it. A stack that already opens
|
|
13
|
+
* with the header — or that its author shaped some other way — is returned untouched.
|
|
14
|
+
*/
|
|
15
|
+
export function failureStack(value: unknown): string | undefined {
|
|
16
|
+
const stack = stringField(value, 'stack');
|
|
17
|
+
if (stack === undefined) return undefined;
|
|
18
|
+
const name = stringField(value, 'name') ?? 'Error';
|
|
19
|
+
const message = stringField(value, 'message') ?? '';
|
|
20
|
+
const header = message === '' ? name : `${name}: ${message}`;
|
|
21
|
+
if (stack.startsWith(header)) return stack;
|
|
22
|
+
const newline = stack.indexOf('\n');
|
|
23
|
+
const first = newline === -1 ? stack : stack.slice(0, newline);
|
|
24
|
+
// The lost header is exactly the base constructor's name with no message after it.
|
|
25
|
+
if (first === 'Error') return `${header}${newline === -1 ? '' : stack.slice(newline)}`;
|
|
26
|
+
if (FRAME_LINE.test(first)) return `${header}\n${stack}`;
|
|
27
|
+
return stack;
|
|
28
|
+
}
|