@ontrails/library 0.2.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/src/surface.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The in-memory library surface: `surface(graph, options)` returns a callable
3
+ * client. It is a peer of the CLI, MCP, and HTTP surfaces — same contract, same
4
+ * shared pipeline — and the first surface whose `surface()` returns a held
5
+ * client rather than opening a long-running endpoint.
6
+ *
7
+ * The client routes execution through the runtime kernel (`kernelRun`), never
8
+ * through ad hoc `@ontrails/core` deep imports, so the standalone trajectory
9
+ * stays a vendoring step. Root-call behavior: unwrap `Result.ok` to a return
10
+ * value, throw on `Result.err`. The held client also exposes a no-throw
11
+ * `result` lane with the same export names so downstream package emission can
12
+ * map that lane to the generated `/result` subpath.
13
+ */
14
+ import { Result, ValidationError } from '@ontrails/core';
15
+
16
+ import { deriveLibraryApi } from './derive.js';
17
+ import type {
18
+ DeriveLibraryApiOptions,
19
+ LibraryExport,
20
+ LibraryRenderingPlan,
21
+ } from './derive.js';
22
+ import { toLibraryError } from './errors.js';
23
+ import type { LibraryError } from './errors.js';
24
+ import { partitionLibraryInput } from './layer-input.js';
25
+ import { kernelRun } from './kernel.js';
26
+ import type { KernelRunOptions, Topo } from './kernel.js';
27
+
28
+ /**
29
+ * Options for the in-memory library surface: rendering selectors plus the
30
+ * runtime context the client owns (e.g. a permit for permitted trails).
31
+ */
32
+ export interface SurfaceLibraryOptions
33
+ extends
34
+ DeriveLibraryApiOptions,
35
+ Omit<KernelRunOptions, 'layerInputs' | 'surfaceLayers' | 'topoLayers'> {}
36
+
37
+ /** A callable library method: validated input in, output out, throws on failure. */
38
+ export type LibraryMethod = (input: unknown) => Promise<unknown>;
39
+
40
+ /** A no-throw library method: validated input in, raw Result boundary out. */
41
+ export type LibraryResultMethod = (
42
+ input: unknown
43
+ ) => Promise<Result<unknown, LibraryError>>;
44
+
45
+ /** The held in-memory client: one method per rendered export, plus the rendering. */
46
+ export interface LibraryClient {
47
+ /** Invoke an exported trail by its consumer-native name. */
48
+ readonly call: Readonly<Record<string, LibraryMethod>>;
49
+ /** Invoke an exported trail by name without unwrapping the Result boundary. */
50
+ readonly result: Readonly<Record<string, LibraryResultMethod>>;
51
+ /** The resolved rendering this client was built from (introspection). */
52
+ readonly rendering: LibraryRenderingPlan;
53
+ }
54
+
55
+ const prepareLibraryInput = (
56
+ entry: LibraryExport,
57
+ input: unknown
58
+ ):
59
+ | {
60
+ readonly layerInputs: Record<string, unknown>;
61
+ readonly trailInput: unknown;
62
+ }
63
+ | ValidationError => {
64
+ const parsed = entry.input.safeParse(input);
65
+ if (!parsed.success) {
66
+ return new ValidationError(
67
+ `Invalid input for library export '${entry.exportName}': ${parsed.error.message}`,
68
+ {
69
+ cause: parsed.error,
70
+ context: { issues: parsed.error.issues, trailId: entry.trailId },
71
+ }
72
+ );
73
+ }
74
+ return partitionLibraryInput(parsed.data, entry.layerInputs);
75
+ };
76
+
77
+ const runtimeOptionsFor = (
78
+ graph: Topo,
79
+ options: SurfaceLibraryOptions
80
+ ): KernelRunOptions => ({
81
+ abortSignal: options.abortSignal,
82
+ configValues: options.configValues,
83
+ createContext: options.createContext,
84
+ ctx: options.ctx,
85
+ dryRun: options.dryRun,
86
+ permit: options.permit,
87
+ resources: options.resources,
88
+ surfaceLayers: options.layers,
89
+ topoLayers: graph.layers,
90
+ version: options.version,
91
+ });
92
+
93
+ export const runLibraryResult = async (
94
+ graph: Topo,
95
+ id: string,
96
+ input: unknown,
97
+ options: SurfaceLibraryOptions = {}
98
+ ): Promise<Result<unknown, LibraryError>> => {
99
+ const rendering = deriveLibraryApi(graph, options);
100
+ const runOptions = runtimeOptionsFor(graph, options);
101
+ const entry = rendering.exports.find((candidate) => candidate.trailId === id);
102
+ const prepared = entry
103
+ ? prepareLibraryInput(entry, input)
104
+ : { layerInputs: {}, trailInput: input };
105
+ if (prepared instanceof ValidationError) {
106
+ return Result.err(toLibraryError(prepared));
107
+ }
108
+ const outcome = await kernelRun(graph, id, prepared.trailInput, {
109
+ ...runOptions,
110
+ ...(Object.keys(prepared.layerInputs).length === 0
111
+ ? {}
112
+ : { layerInputs: prepared.layerInputs }),
113
+ });
114
+ return outcome.mapErr(toLibraryError);
115
+ };
116
+
117
+ /**
118
+ * Materialize a topo as an in-memory library client. Each rendered export
119
+ * becomes a method that executes its trail through the shared pipeline and
120
+ * unwraps the Result — returning the value or throwing the error. The same
121
+ * export names are available under `result` for callers that want the raw
122
+ * `Result` boundary.
123
+ *
124
+ * @example
125
+ * const lib = await surface(app);
126
+ * const widget = await lib.call.widgetGet({ id: '1' });
127
+ */
128
+ export const surface = async (
129
+ graph: Topo,
130
+ options: SurfaceLibraryOptions = {}
131
+ // oxlint-disable-next-line require-await -- async to match peer surfaces and allow future resource init
132
+ ): Promise<LibraryClient> => {
133
+ const rendering = deriveLibraryApi(graph, options);
134
+ const runOptions = runtimeOptionsFor(graph, options);
135
+ const call: Record<string, LibraryMethod> = {};
136
+ const result: Record<string, LibraryResultMethod> = {};
137
+
138
+ for (const entry of rendering.exports) {
139
+ const runExport: LibraryResultMethod = async (input: unknown) => {
140
+ const prepared = prepareLibraryInput(entry, input);
141
+ if (prepared instanceof ValidationError) {
142
+ return Result.err(toLibraryError(prepared));
143
+ }
144
+ const outcome = await kernelRun(
145
+ graph,
146
+ entry.trailId,
147
+ prepared.trailInput,
148
+ {
149
+ ...runOptions,
150
+ ...(Object.keys(prepared.layerInputs).length === 0
151
+ ? {}
152
+ : { layerInputs: prepared.layerInputs }),
153
+ }
154
+ );
155
+ return outcome.mapErr(toLibraryError);
156
+ };
157
+ result[entry.exportName] = runExport;
158
+
159
+ call[entry.exportName] = async (input: unknown): Promise<unknown> => {
160
+ const outcome = await runExport(input);
161
+ if (outcome.isErr()) {
162
+ throw outcome.error;
163
+ }
164
+ return outcome.value;
165
+ };
166
+ }
167
+
168
+ return {
169
+ call: Object.freeze(call),
170
+ rendering,
171
+ result: Object.freeze(result),
172
+ };
173
+ };