@telorun/sdk 0.48.0 → 0.50.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.
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/invoke-error.d.ts +3 -1
- package/dist/invoke-error.d.ts.map +1 -1
- package/dist/invoke-error.js +14 -1
- package/dist/log-record.d.ts +72 -0
- package/dist/log-record.d.ts.map +1 -0
- package/dist/log-record.js +34 -0
- package/dist/log-severity.d.ts +55 -0
- package/dist/log-severity.d.ts.map +1 -0
- package/dist/log-severity.js +110 -0
- package/dist/log-sink.d.ts +91 -0
- package/dist/log-sink.d.ts.map +1 -0
- package/dist/log-sink.js +16 -0
- package/dist/logger.d.ts +85 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +30 -0
- package/dist/network-fetch.d.ts +71 -0
- package/dist/network-fetch.d.ts.map +1 -0
- package/dist/network-fetch.js +140 -0
- package/dist/resource-context.d.ts +24 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-instance.d.ts +17 -0
- package/dist/resource-instance.d.ts.map +1 -1
- package/dist/resource-instance.js +4 -0
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/invoke-error.ts +14 -1
- package/src/log-record.ts +113 -0
- package/src/log-severity.ts +128 -0
- package/src/log-sink.ts +115 -0
- package/src/logger.ts +125 -0
- package/src/network-fetch.ts +175 -0
- package/src/resource-context.ts +24 -0
- package/src/resource-instance.ts +18 -0
package/src/log-sink.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { LogRecord } from "./log-record.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `Telo.Sink` capability contract — `kernel/specs/logging.md` §10.
|
|
5
|
+
*
|
|
6
|
+
* This lives in the SDK rather than the kernel because §10.2 makes the sink set
|
|
7
|
+
* **open to the ecosystem**: a third party ships a sink by publishing a module
|
|
8
|
+
* whose kind extends `Telo.LogSink`. That module is an ordinary module author's
|
|
9
|
+
* artifact, so the contract it implements belongs on the module-author surface,
|
|
10
|
+
* not behind a kernel-internal import.
|
|
11
|
+
*
|
|
12
|
+
* The logger writes to a sink through this contract directly and **never**
|
|
13
|
+
* through `ctx.invoke`: per-record dispatch is far too slow for a logging hot
|
|
14
|
+
* path, and the dispatch chokepoint emits trace events, so logging through it
|
|
15
|
+
* would generate telemetry from inside the telemetry path.
|
|
16
|
+
*
|
|
17
|
+
* The contract is deliberately payload-opaque — no filtering, no encoding —
|
|
18
|
+
* which is what lets a future `Telo.TraceSink` reuse the capability with a
|
|
19
|
+
* different record type. Log-specific configuration lives on the
|
|
20
|
+
* `Telo.LogSink` abstract instead.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type DropCause = "buffer_full" | "sampled" | "encode_failure" | "sink_error";
|
|
24
|
+
|
|
25
|
+
/** Policy for a saturated buffer (§10.3). A runtime that cannot honour `block`
|
|
26
|
+
* rejects the manifest at load rather than silently substituting a dropping
|
|
27
|
+
* policy. */
|
|
28
|
+
export type OnFull = "block" | "drop_new" | "drop_old";
|
|
29
|
+
|
|
30
|
+
export interface SinkBufferPolicy {
|
|
31
|
+
/** Bounded. Never unbounded. */
|
|
32
|
+
buffer: number;
|
|
33
|
+
onFull: OnFull;
|
|
34
|
+
/** Max time a record may sit buffered, in milliseconds. */
|
|
35
|
+
flushIntervalMs: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const DEFAULT_BUFFER_POLICY: SinkBufferPolicy = {
|
|
39
|
+
buffer: 8192,
|
|
40
|
+
onFull: "drop_new",
|
|
41
|
+
flushIntervalMs: 1000,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface LogSinkInstance {
|
|
45
|
+
/** Identity for drop accounting (§10.4): the resource name for a `!ref`, or
|
|
46
|
+
* kind plus position for an inline definition. */
|
|
47
|
+
readonly sinkId: string;
|
|
48
|
+
|
|
49
|
+
/** This sink's own fan-out filter, applied *after* the record is created. It
|
|
50
|
+
* never decides whether a record is created at all — that is the pipeline's
|
|
51
|
+
* minimum-level gate. */
|
|
52
|
+
readonly level: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether the sink can be drained to its destination from inside a
|
|
56
|
+
* synchronous call, with no scheduler turn. A file descriptor write can; a
|
|
57
|
+
* network round-trip cannot, and neither can a transport living on another
|
|
58
|
+
* thread — the producer cannot drain a queue it does not own.
|
|
59
|
+
*
|
|
60
|
+
* A capability tier, not a language carve-out: the same rule makes an OTLP
|
|
61
|
+
* sink best-effort in Rust and Go, where blocking a producer thread is
|
|
62
|
+
* possible but still would not make a round-trip synchronous.
|
|
63
|
+
*/
|
|
64
|
+
readonly syncFlushable: boolean;
|
|
65
|
+
|
|
66
|
+
/** Accept a record. MUST NOT throw — a sink failure is reported out-of-band
|
|
67
|
+
* and counted, never propagated to the caller (§8.4). */
|
|
68
|
+
write(record: LogRecord): void;
|
|
69
|
+
|
|
70
|
+
/** Drain asynchronously. */
|
|
71
|
+
flush(): Promise<void>;
|
|
72
|
+
|
|
73
|
+
/** Drain to completion before returning. A no-op when {@link syncFlushable}
|
|
74
|
+
* is `false`; the `fatal` path initiates those sinks' flushes without
|
|
75
|
+
* waiting, because blocking on a sink it cannot synchronously drain is a
|
|
76
|
+
* deadlock on an event loop, not durability. */
|
|
77
|
+
flushSync(): void;
|
|
78
|
+
|
|
79
|
+
/** Release the destination. Called during teardown, after the final flush. */
|
|
80
|
+
close(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The pipeline surface a sink controller reaches for — attach, detach, resolve a
|
|
85
|
+
* level, count a drop. Deliberately narrow: everything else about the pipeline
|
|
86
|
+
* stays private to the runtime, so a third-party sink depends on this and
|
|
87
|
+
* nothing deeper.
|
|
88
|
+
*/
|
|
89
|
+
export interface LoggingHost {
|
|
90
|
+
attach(sink: LogSinkInstance): void;
|
|
91
|
+
detach(sink: LogSinkInstance): void;
|
|
92
|
+
/** Resolve a sink's declared `level:` to a severity number, falling back to
|
|
93
|
+
* the effective scope threshold when the sink declares none (§12.1). */
|
|
94
|
+
levelFor(level: string | undefined): number;
|
|
95
|
+
/** Count `count` dropped records against this sink so §10.4's accounting stays
|
|
96
|
+
* complete. `count` defaults to 1; a sink that loses a whole batch at once
|
|
97
|
+
* (an OTLP export failure) passes the batch size so the total is not
|
|
98
|
+
* undercounted to one-per-failure. */
|
|
99
|
+
recordDrop(sinkId: string, cause: DropCause, count?: number): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The diagnostic §10.3 requires when a runtime cannot honour `on_full: block`.
|
|
103
|
+
* Rejecting is deliberate: `on_full` exists so an operator can state durability
|
|
104
|
+
* intent, and silently substituting a dropping policy hands back the opposite
|
|
105
|
+
* guarantee — discovered from a gap in an audit trail rather than from an
|
|
106
|
+
* error. */
|
|
107
|
+
export function blockUnsupportedMessage(sinkId: string): string {
|
|
108
|
+
return (
|
|
109
|
+
`Sink "${sinkId}": on_full: block is not supported by this runtime ` +
|
|
110
|
+
`(single-threaded event loop — blocking the producer would stall the writer). ` +
|
|
111
|
+
`Use \`drop_new\` or \`drop_old\`, or move this sink to a worker thread.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const BLOCK_UNSUPPORTED = "ERR_LOG_SINK_ON_FULL_UNSUPPORTED";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { AnyValue, LogAttributes } from "./log-record.js";
|
|
2
|
+
import type { SeverityNumber } from "./log-severity.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The logger surface every Telo runtime exposes — `kernel/specs/logging.md` §8.
|
|
6
|
+
*
|
|
7
|
+
* Reached ambiently as `ctx.log`. The logger is ambient rather than a resource
|
|
8
|
+
* because it must work before any resource initializes; its *sinks* are
|
|
9
|
+
* resources, which is what keeps the destination set open to the ecosystem.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A value resolved only on the emit path (§8.2) — slog's `LogValuer`, zap's
|
|
14
|
+
* `ObjectMarshaler`, `tracing`'s `Value`. A deferred value attached to a
|
|
15
|
+
* suppressed record is never resolved, so an expensive rendering costs nothing
|
|
16
|
+
* below the threshold. This is RECOMMENDED sugar and does **not** substitute for
|
|
17
|
+
* {@link Logger.enabled}, which is the only mechanism that avoids evaluating a
|
|
18
|
+
* call's *arguments*.
|
|
19
|
+
*/
|
|
20
|
+
export interface LogValuer {
|
|
21
|
+
toLogValue(): AnyValue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type LogAttributeInput = AnyValue | LogValuer;
|
|
25
|
+
export type LogAttributesInput = Record<string, LogAttributeInput>;
|
|
26
|
+
|
|
27
|
+
/** Per-record extras that are top-level record fields rather than attributes.
|
|
28
|
+
* Kept out of the attribute map so they cannot collide with a reserved key. */
|
|
29
|
+
export interface LogOptions {
|
|
30
|
+
/** Any thrown value. Normalized to the record's `error` (§4.2), with the
|
|
31
|
+
* `cause` chain bounded per §6.3. */
|
|
32
|
+
error?: unknown;
|
|
33
|
+
/** Identifies a class of event; max 256 chars. Bridges to the event bus. */
|
|
34
|
+
eventName?: string;
|
|
35
|
+
/** When the event occurred, if earlier than the moment `log()` was called —
|
|
36
|
+
* set by a bridge, which also stamps `observedTimestamp` (§13.3). */
|
|
37
|
+
timestamp?: bigint;
|
|
38
|
+
/** The original source spelling of the level, preserved when bridging a level
|
|
39
|
+
* Telo does not name (§5.1). Defaults to the canonical short name. */
|
|
40
|
+
severityText?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Logger {
|
|
44
|
+
/**
|
|
45
|
+
* Whether a record at this severity would reach any sink. The load-bearing
|
|
46
|
+
* performance primitive: guard an expensive call with it so the *arguments*
|
|
47
|
+
* are never evaluated.
|
|
48
|
+
*
|
|
49
|
+
* Never blocks, never throws. The result is **not static** — it changes when
|
|
50
|
+
* configuration changes or a sink attaches or detaches (§12.4), so callers
|
|
51
|
+
* re-check per emission rather than caching a boolean.
|
|
52
|
+
*/
|
|
53
|
+
enabled(severity: SeverityNumber): boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Emit a record. Never throws, under any condition, including sink failure —
|
|
57
|
+
* a sink failure is reported on the fallback diagnostic stream and counted,
|
|
58
|
+
* never propagated and never swallowed (§8.4).
|
|
59
|
+
*/
|
|
60
|
+
log(
|
|
61
|
+
severity: SeverityNumber,
|
|
62
|
+
message: string,
|
|
63
|
+
attributes?: LogAttributesInput,
|
|
64
|
+
options?: LogOptions,
|
|
65
|
+
): void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A child logger whose bound attributes are merged into every record it emits.
|
|
69
|
+
* Record attributes override bound attributes. Binding is O(1) amortized: the
|
|
70
|
+
* merge happens once here, never per record.
|
|
71
|
+
*/
|
|
72
|
+
with(attributes: LogAttributesInput): Logger;
|
|
73
|
+
|
|
74
|
+
/** Drain every attached sink. Bounded by the caller; see §10.5. */
|
|
75
|
+
flush(): Promise<void>;
|
|
76
|
+
|
|
77
|
+
trace(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
|
|
78
|
+
debug(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
|
|
79
|
+
info(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
|
|
80
|
+
warn(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
|
|
81
|
+
error(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
|
|
82
|
+
/**
|
|
83
|
+
* Emits at severity 21. Severity never implies control flow (§5, D5): `fatal`
|
|
84
|
+
* does **not** terminate the process, exit, or panic — it triggers an
|
|
85
|
+
* immediate flush, synchronous on every sink that supports it and best-effort
|
|
86
|
+
* on the rest (§10.5).
|
|
87
|
+
*/
|
|
88
|
+
fatal(message: string, attributes?: LogAttributesInput, options?: LogOptions): void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolve a {@link LogValuer} if the value is one, else pass it through. */
|
|
92
|
+
export function isLogValuer(value: unknown): value is LogValuer {
|
|
93
|
+
return (
|
|
94
|
+
typeof value === "object" &&
|
|
95
|
+
value !== null &&
|
|
96
|
+
typeof (value as LogValuer).toLogValue === "function"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A logger that discards everything. Used where a logger is structurally
|
|
101
|
+
* required before one is available, and by tests that assert silence. */
|
|
102
|
+
export const NOOP_LOGGER: Logger = {
|
|
103
|
+
enabled: () => false,
|
|
104
|
+
log: () => {},
|
|
105
|
+
with: () => NOOP_LOGGER,
|
|
106
|
+
flush: async () => {},
|
|
107
|
+
trace: () => {},
|
|
108
|
+
debug: () => {},
|
|
109
|
+
info: () => {},
|
|
110
|
+
warn: () => {},
|
|
111
|
+
error: () => {},
|
|
112
|
+
fatal: () => {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** The bound-attribute merge of {@link Logger.with}, exposed so a runtime's
|
|
116
|
+
* child-logger implementation and its conformance vectors share one definition
|
|
117
|
+
* of "record attributes win". */
|
|
118
|
+
export function mergeBoundAttributes(
|
|
119
|
+
bound: LogAttributes | undefined,
|
|
120
|
+
record: LogAttributes | undefined,
|
|
121
|
+
): LogAttributes | undefined {
|
|
122
|
+
if (!bound) return record;
|
|
123
|
+
if (!record) return bound;
|
|
124
|
+
return { ...bound, ...record };
|
|
125
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isCancellationError } from "./cancellation.js";
|
|
2
|
+
import { InvokeError } from "./invoke-error.js";
|
|
3
|
+
|
|
4
|
+
/** Raised when a request never reached the peer — DNS, connect, reset, or TLS
|
|
5
|
+
* trust. Distinct from a non-OK HTTP response, which is a reply the caller
|
|
6
|
+
* interprets itself. */
|
|
7
|
+
export const ERR_NETWORK_UNREACHABLE = "ERR_NETWORK_UNREACHABLE";
|
|
8
|
+
|
|
9
|
+
/** The facts a transport failure carries. Deliberately structured rather than
|
|
10
|
+
* pre-rendered: a controller reports *what happened*, and whoever displays the
|
|
11
|
+
* error turns it into a sentence. Prose baked into a controller would have to
|
|
12
|
+
* be re-typed identically by every language SDK (TS, Rust, Go, …) and would
|
|
13
|
+
* drift; `cause: "ENOTFOUND"` is the same symbol in every language.
|
|
14
|
+
*
|
|
15
|
+
* `message` on the thrown error is a reasonable default for today's renderers;
|
|
16
|
+
* a kernel-side renderer can format from these fields instead. */
|
|
17
|
+
export interface NetworkErrorData {
|
|
18
|
+
/** What was being attempted, e.g. `"Embedding model request"`. */
|
|
19
|
+
operation: string;
|
|
20
|
+
url: string;
|
|
21
|
+
host: string;
|
|
22
|
+
port?: number;
|
|
23
|
+
/** The OS/undici code — `ENOTFOUND`, `ECONNREFUSED`, `CERT_HAS_EXPIRED`, … */
|
|
24
|
+
cause: string;
|
|
25
|
+
/** The underlying error's own message, kept verbatim. Carries detail no code
|
|
26
|
+
* mapping has (`SSL alert number 80`, a resolver's remarks), so wrapping is
|
|
27
|
+
* never a downgrade on an unmapped code. */
|
|
28
|
+
detail?: string;
|
|
29
|
+
/** `metadata.name` of the resource whose configuration produced `url`, so the
|
|
30
|
+
* error names the actual instance rather than its kind. */
|
|
31
|
+
resource?: string;
|
|
32
|
+
/** The setting to change — a manifest field (`baseUrl`) or a CLI/env name
|
|
33
|
+
* (`--registry`). Structured rather than a pre-written sentence: this is the
|
|
34
|
+
* one genuinely actionable part, and prose here would be the thing every
|
|
35
|
+
* other language SDK has to retype and keep in sync. */
|
|
36
|
+
setting?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Human explanation per transport failure code. Interpolates only the facts
|
|
40
|
+
* already in `NetworkErrorData`, so a renderer in another language can produce
|
|
41
|
+
* the same sentence from the same fields. */
|
|
42
|
+
function explain(code: string, host: string, port?: number): string {
|
|
43
|
+
const target = port ? `${host}:${port}` : host;
|
|
44
|
+
switch (code) {
|
|
45
|
+
case "ENOTFOUND":
|
|
46
|
+
return `DNS lookup failed for host '${host}' — the name does not resolve`;
|
|
47
|
+
case "EAI_AGAIN":
|
|
48
|
+
return `DNS is temporarily unavailable resolving '${host}' — a transient resolver failure`;
|
|
49
|
+
case "ECONNREFUSED":
|
|
50
|
+
return `nothing is listening on ${target}`;
|
|
51
|
+
case "ECONNRESET":
|
|
52
|
+
return `the connection to ${target} was reset by the peer`;
|
|
53
|
+
case "EHOSTUNREACH":
|
|
54
|
+
return `no network route to ${host}`;
|
|
55
|
+
case "ETIMEDOUT":
|
|
56
|
+
case "UND_ERR_CONNECT_TIMEOUT":
|
|
57
|
+
return `the connection to ${target} timed out`;
|
|
58
|
+
case "CERT_HAS_EXPIRED":
|
|
59
|
+
return `the TLS certificate presented by ${host} has expired`;
|
|
60
|
+
case "DEPTH_ZERO_SELF_SIGNED_CERT":
|
|
61
|
+
case "SELF_SIGNED_CERT_IN_CHAIN":
|
|
62
|
+
return `the TLS certificate presented by ${host} is self-signed and not trusted`;
|
|
63
|
+
case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
|
|
64
|
+
return `the TLS certificate chain presented by ${host} could not be verified`;
|
|
65
|
+
case "EPROTO":
|
|
66
|
+
return `the TLS handshake with ${host} failed`;
|
|
67
|
+
default:
|
|
68
|
+
return `the request to ${target} failed at the transport layer`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The deepest `message` in the cause chain — the one carrying detail a code
|
|
73
|
+
* mapping cannot have. Skips undici's own `"fetch failed"`, which is the
|
|
74
|
+
* placeholder this whole module exists to replace. */
|
|
75
|
+
function causeDetail(err: unknown): string | undefined {
|
|
76
|
+
let current: unknown = err;
|
|
77
|
+
let detail: string | undefined;
|
|
78
|
+
for (let depth = 0; current && depth < 5; depth++) {
|
|
79
|
+
const message = (current as { message?: unknown }).message;
|
|
80
|
+
if (typeof message === "string" && message && message !== "fetch failed") detail = message;
|
|
81
|
+
current = (current as { cause?: unknown }).cause;
|
|
82
|
+
}
|
|
83
|
+
return detail;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Walk the cause chain for the first `code` — undici wraps the real error one or
|
|
88
|
+
* more levels down, which is exactly the detail lost when only `message` is
|
|
89
|
+
* reported.
|
|
90
|
+
*
|
|
91
|
+
* Exported because classifying a network failure by substring-matching the
|
|
92
|
+
* message does not work: `fetch` rejects with the literal text `"fetch failed"`
|
|
93
|
+
* for DNS, refusal, and TLS alike, so a `message.includes("enotfound")` test
|
|
94
|
+
* silently never matches and every failure collapses into whichever branch is
|
|
95
|
+
* last. Callers with their own error contract should classify on this code
|
|
96
|
+
* rather than on prose.
|
|
97
|
+
*/
|
|
98
|
+
export function networkCauseCode(err: unknown): string | undefined {
|
|
99
|
+
let current: unknown = err;
|
|
100
|
+
for (let depth = 0; current && depth < 5; depth++) {
|
|
101
|
+
const code = (current as { code?: unknown }).code;
|
|
102
|
+
if (typeof code === "string") return code;
|
|
103
|
+
current = (current as { cause?: unknown }).cause;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `fetch` that turns a transport-level failure into an {@link InvokeError}
|
|
110
|
+
* carrying {@link NetworkErrorData}, instead of undici's opaque
|
|
111
|
+
* `TypeError: fetch failed` whose real cause sits unread on `error.cause`.
|
|
112
|
+
*
|
|
113
|
+
* Only *transport* failures are wrapped. A non-OK response is returned
|
|
114
|
+
* untouched, because a status code is a reply the caller interprets (and often
|
|
115
|
+
* renders from the provider's own error body) — so this drops into an existing
|
|
116
|
+
* call site without changing status handling. Cancellation is re-thrown as-is:
|
|
117
|
+
* an aborted request is the caller's intent, not a network fault.
|
|
118
|
+
*
|
|
119
|
+
* @param context.operation What is being attempted, for the message.
|
|
120
|
+
* @param context.resource `metadata.name` of the resource whose configuration
|
|
121
|
+
* produced the URL, so the error names the instance, not just its kind.
|
|
122
|
+
* @param context.setting The manifest field or CLI/env name to change. Passed
|
|
123
|
+
* as a bare identifier, never a sentence — the wording is composed here, in
|
|
124
|
+
* one place, so another language's SDK supplies the same two facts rather
|
|
125
|
+
* than retyping the same English.
|
|
126
|
+
*/
|
|
127
|
+
export async function fetchOrThrow(
|
|
128
|
+
input: string | URL,
|
|
129
|
+
init: RequestInit | undefined,
|
|
130
|
+
context: { operation: string; resource?: string; setting?: string },
|
|
131
|
+
): Promise<Response> {
|
|
132
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
133
|
+
try {
|
|
134
|
+
return await fetch(input, init);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (isCancellationError(err)) throw err;
|
|
137
|
+
if (err instanceof DOMException && err.name === "AbortError") throw err;
|
|
138
|
+
|
|
139
|
+
const code = networkCauseCode(err) ?? "UNKNOWN";
|
|
140
|
+
let host = url;
|
|
141
|
+
let port: number | undefined;
|
|
142
|
+
try {
|
|
143
|
+
const parsed = new URL(url);
|
|
144
|
+
host = parsed.hostname;
|
|
145
|
+
if (parsed.port) port = Number(parsed.port);
|
|
146
|
+
} catch {
|
|
147
|
+
// Non-absolute URL — fall back to the raw string as the host label.
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const detail = causeDetail(err);
|
|
151
|
+
const data: NetworkErrorData = { operation: context.operation, url, host, cause: code };
|
|
152
|
+
if (port !== undefined) data.port = port;
|
|
153
|
+
if (detail) data.detail = detail;
|
|
154
|
+
if (context.resource) data.resource = context.resource;
|
|
155
|
+
if (context.setting) data.setting = context.setting;
|
|
156
|
+
|
|
157
|
+
// `detail` is appended only when the code has no mapping of its own —
|
|
158
|
+
// otherwise the explanation already says it better. Without this, wrapping
|
|
159
|
+
// an unmapped code would *lose* information relative to the raw error.
|
|
160
|
+
const explained = explain(code, host, port);
|
|
161
|
+
const isMapped = !explained.startsWith("the request to");
|
|
162
|
+
const because = isMapped || !detail ? explained : `${explained} (${detail})`;
|
|
163
|
+
|
|
164
|
+
const fix = context.setting
|
|
165
|
+
? ` Check \`${context.setting}\`${context.resource ? ` on resource '${context.resource}'` : ""}.`
|
|
166
|
+
: "";
|
|
167
|
+
|
|
168
|
+
const message =
|
|
169
|
+
`${context.operation} failed: cannot reach ${url} — ${code}: ${because}.${fix}`;
|
|
170
|
+
|
|
171
|
+
// The wrapped error stays reachable as `cause`: the code mapping is a
|
|
172
|
+
// convenience, never a reason to destroy what was actually thrown.
|
|
173
|
+
throw new InvokeError(ERR_NETWORK_UNREACHABLE, message, data, { cause: err });
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/resource-context.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CancellationSource, InvokeContext, OpenSpan, OpenSpanOptions } from "./cancellation.js";
|
|
2
2
|
import { ControllerContext } from "./controller-context.js";
|
|
3
|
+
import type { Logger } from "./logger.js";
|
|
4
|
+
import type { LoggingHost } from "./log-sink.js";
|
|
3
5
|
import { ControllerPolicy } from "./controller-policy.js";
|
|
4
6
|
import { EvaluationContext } from "./evaluation-context.js";
|
|
5
7
|
import { ModuleContext } from "./module-context.js";
|
|
@@ -131,6 +133,28 @@ export interface ResourceContext extends ControllerContext {
|
|
|
131
133
|
* manifests. Use this when you need the full kind surface area visible from
|
|
132
134
|
* the module. */
|
|
133
135
|
loadManifests(url: string): Promise<ResourceManifest[]>;
|
|
136
|
+
/**
|
|
137
|
+
* The structured logger for this resource — `kernel/specs/logging.md` §13.2.
|
|
138
|
+
*
|
|
139
|
+
* Ambient rather than a resource (D3), because it must work before any
|
|
140
|
+
* resource initializes. Records are automatically stamped with this
|
|
141
|
+
* resource's identity, its module, its import-alias scope, and the active
|
|
142
|
+
* dispatch span's trace and span ids — a controller never passes those.
|
|
143
|
+
*
|
|
144
|
+
* A controller emits diagnostics **only** through this. Writing to
|
|
145
|
+
* stdout/stderr for diagnostic purposes is forbidden; writing to stdout as
|
|
146
|
+
* *data* (as the `Console` module does) is a separate, legitimate concern and
|
|
147
|
+
* is unaffected.
|
|
148
|
+
*/
|
|
149
|
+
readonly log: Logger;
|
|
150
|
+
/**
|
|
151
|
+
* Sink attach/detach and drop accounting — the surface a `Telo.Sink`
|
|
152
|
+
* controller needs and nothing else. §10.2 keeps the sink set open to the
|
|
153
|
+
* ecosystem, so a third-party sink module reaches the pipeline through this
|
|
154
|
+
* rather than through a kernel-internal import. Ordinary controllers use
|
|
155
|
+
* {@link log}.
|
|
156
|
+
*/
|
|
157
|
+
readonly logging: LoggingHost;
|
|
134
158
|
readonly moduleContext: ModuleContext;
|
|
135
159
|
readonly env: Record<string, string | undefined>;
|
|
136
160
|
readonly stdin: NodeJS.ReadableStream;
|
package/src/resource-instance.ts
CHANGED
|
@@ -11,8 +11,26 @@ export type ResourceInstance<TInput = Record<string, any>, TOutput = any> = Part
|
|
|
11
11
|
init?(ctx?: ResourceContext): Promise<void>;
|
|
12
12
|
teardown?(): void | Promise<void>;
|
|
13
13
|
snapshot?(): Record<string, any> | Promise<Record<string, any>>;
|
|
14
|
+
/**
|
|
15
|
+
* Teardown ordering hint. Instances tear down in ascending priority — a
|
|
16
|
+
* higher number means *later*. Default `0`; within one priority the base
|
|
17
|
+
* order (reverse init) is preserved.
|
|
18
|
+
*
|
|
19
|
+
* This exists because the base order is reverse *insertion* order, which the
|
|
20
|
+
* multi-pass init retry can perturb, so a resource that must reliably outlive
|
|
21
|
+
* the rest at shutdown cannot express that through the dependency graph. Log
|
|
22
|
+
* sinks set {@link TEARDOWN_LAST} so they flush after every resource that
|
|
23
|
+
* might log while shutting down — a generic mechanism, not a logging-specific
|
|
24
|
+
* carve-out in the teardown path.
|
|
25
|
+
*/
|
|
26
|
+
teardownPriority?: number;
|
|
14
27
|
};
|
|
15
28
|
|
|
29
|
+
/** Teardown-last priority (see {@link ResourceInstance.teardownPriority}). Log
|
|
30
|
+
* sinks use it so anything logging during its own teardown still reaches a live
|
|
31
|
+
* destination. */
|
|
32
|
+
export const TEARDOWN_LAST = 1000;
|
|
33
|
+
|
|
16
34
|
/** The kind+name an instance was resolved from. */
|
|
17
35
|
export interface RefIdentity {
|
|
18
36
|
kind: string;
|