@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 CHANGED
@@ -6,14 +6,14 @@ change to every package.
6
6
 
7
7
  | Rule | |
8
8
  |---|---|
9
- | Deps | `@ultimat3/schema` only (`bun-types` for 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.1",
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.1"
40
+ "@ultimat3/schema": "27.10.2"
41
41
  }
42
42
  }
@@ -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 — only when the answer carried
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
@@ -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;
@@ -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, stringField } from './error-render';
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
- // `stringField`, because `error instanceof Error` and `.stack` are both property operations
185
- // on a caught value — and this function's own contract is that reporting never throws.
186
- stack: stringField(error, 'stack') ?? normalized.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
+ }
@@ -44,6 +44,7 @@ export {
44
44
  retryFor,
45
45
  statedDelayMs,
46
46
  } from '../error-retry';
47
+ export { failureStack } from '../error-stack';
47
48
  export type {
48
49
  CodedErrorInit,
49
50
  ErrorAudience,
package/src/index.ts CHANGED
@@ -309,6 +309,7 @@ export {
309
309
  ERROR_RETRY_KINDS,
310
310
  errorCodeSnapshot,
311
311
  errorRetry,
312
+ failureStack,
312
313
  fixFor,
313
314
  formatError,
314
315
  hasErrorCode,