Repository navigation
Expand file tree
/
Copy pathbackendRegistry.ts
More file actions
159 lines (151 loc) · 5.85 KB
/
Copy pathbackendRegistry.ts
File metadata and controls
159 lines (151 loc) · 5.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { AgentConfig, AgentSettings, BackendDescriptor, object } from './protocol';
import { SessionBackend } from './backend';
import { SessBackend, SessOptions } from './backends/sessBackend';
import { installAgentBundle } from './launcher';
import { installSessRuntime } from './backends/sessPreparation';
import { resolveArfExecutable } from './arfExecutable';
export interface PreparationContext {
extensionPath: string;
root: string;
log(text: string): void;
}
export interface BackendDefinition {
preflight(descriptor: BackendDescriptor, settings: AgentSettings): BackendDescriptor;
prepare(descriptor: BackendDescriptor, context: PreparationContext): Promise<BackendDescriptor>;
create(descriptor: BackendDescriptor, settings: AgentSettings): SessionBackend;
}
function sessOptions(descriptor: BackendDescriptor, prepared = true): SessOptions {
const options = object(descriptor.options);
for (const key of prepared ? ['rPath', 'library', 'resources'] : ['rPath']) {
if (typeof options[key] !== 'string' || !options[key]) {
throw new Error(`Missing sess backend ${key}`);
}
}
if (options.frontend === 'r') {
throw new Error(
'R Interactive now requires arf. Start an arf session or use an ordinary R terminal.',
);
}
if (
options.frontend !== 'arf' ||
!['managed', 'adopted'].includes(String(options.ownership)) ||
!['auto', 'jgd', 'standard'].includes(String(options.plotBackend))
) {
throw new Error('Invalid sess backend configuration');
}
if (
options.ownership === 'adopted' &&
(typeof options.arfEndpoint !== 'string' || !options.arfEndpoint)
) {
throw new Error('Missing arf endpoint');
}
return options as unknown as SessOptions;
}
const definitions: Readonly<Record<string, BackendDefinition>> = {
sess: {
preflight(descriptor, settings) {
const options = sessOptions(descriptor, false);
if (options.frontend === 'arf' && options.ownership === 'managed') {
const arfPath = resolveArfExecutable(options.arfPath ?? 'arf', settings.directory);
if (!arfPath) {
throw new Error(
'Cannot start Headless arf. Install arf or set r.interactive.arfPath to its executable.',
);
}
return { kind: 'sess', options: { ...options, arfPath } };
}
return descriptor;
},
async prepare(descriptor, context) {
const options = sessOptions(descriptor, false);
const runtime = await installSessRuntime(
context.extensionPath,
context.root,
options.rPath,
(text) => context.log(text),
);
return { kind: 'sess', options: { ...options, ...runtime } };
},
create(descriptor, settings) {
return new SessBackend(settings, sessOptions(descriptor));
},
},
};
export function backendDefinition(kind: string): BackendDefinition {
const definition = Object.hasOwn(definitions, kind) && definitions[kind];
if (!definition) {
throw new Error(`Unsupported Interactive backend: ${kind}`);
}
return definition;
}
/** The only conversion from flat, pre-abstraction agent configs. */
export function backendDescriptor(config: AgentConfig): BackendDescriptor {
if (config.backend) {
return config.backend;
}
if (!['r', 'arf', 'arf-existing'].includes(config.provider)) {
throw new Error('Unsupported Interactive provider');
}
return {
kind: 'sess',
options: {
frontend: config.provider === 'r' ? 'r' : 'arf',
ownership: config.provider === 'arf-existing' ? 'adopted' : 'managed',
rPath: config.rPath,
library: config.library,
resources: config.resources,
arfPath: config.arfPath,
arfEndpoint: config.arfEndpoint,
plotBackend: config.plotBackend ?? 'auto',
},
};
}
/** Keep old extension readers working, deriving all legacy fields from the descriptor. */
export function withBackend(config: AgentConfig, backend: BackendDescriptor): AgentConfig {
const common = { ...config };
for (const key of [
'rPath',
'library',
'resources',
'arfPath',
'arfEndpoint',
'plotBackend',
] as const) {
delete common[key];
}
if (backend.kind !== 'sess') {
return { ...common, backend };
}
const options = sessOptions(backend);
return {
...common,
backend,
rPath: options.rPath,
library: options.library,
resources: options.resources,
arfPath: options.arfPath,
arfEndpoint: options.arfEndpoint,
plotBackend: options.plotBackend,
provider:
options.frontend === 'r'
? 'r'
: options.ownership === 'adopted'
? 'arf-existing'
: 'arf',
};
}
export function createBackend(config: AgentConfig): SessionBackend {
const descriptor = backendDescriptor(config);
return backendDefinition(descriptor.kind).create(descriptor, config);
}
/** Selection is fixed internally; the optional definition is a test injection seam. */
export async function prepareBackendRuntime(
config: AgentConfig,
context: PreparationContext,
definition = backendDefinition(backendDescriptor(config).kind),
): Promise<{ config: AgentConfig; agent: string }> {
const selected = definition.preflight(backendDescriptor(config), config);
const backend = definition.preflight(await definition.prepare(selected, context), config);
const agent = installAgentBundle(context.extensionPath, context.root);
return { config: withBackend(config, backend), agent };
}