UNPKG

ses

Version:

Hardened JavaScript for Fearless Cooperation

662 lines (609 loc) 20.1 kB
import { getEnvironmentOption as getenv } from '@endo/env-options'; import { Map, Set, TypeError, arrayJoin, arrayMap, arrayPush, arraySome, create, freeze, generatorNext, generatorThrow, getOwnPropertyNames, isArray, isObject, mapGet, mapHas, mapSet, promiseThen, setAdd, values, weakmapGet, weakmapHas, } from './commons.js'; import { makeError, annotateError, q, b, X } from './error/assert.js'; const noop = () => {}; const asyncTrampoline = async (generatorFunc, args, errorWrapper) => { await null; const iterator = generatorFunc(...args); let result = generatorNext(iterator); while (!result.done) { try { // eslint-disable-next-line no-await-in-loop const val = await result.value; result = generatorNext(iterator, val); } catch (error) { result = generatorThrow(iterator, errorWrapper(error)); } } return result.value; }; const syncTrampoline = (generatorFunc, args) => { const iterator = generatorFunc(...args); let result = generatorNext(iterator); while (!result.done) { try { result = generatorNext(iterator, result.value); } catch (error) { result = generatorThrow(iterator, error); } } return result.value; }; // `makeAlias` constructs compartment specifier tuples for the `aliases` // private field of compartments. // These aliases allow a compartment to alias an internal module specifier to a // module specifier in an external compartment, and also to create internal // aliases. // Both are facilitated by the moduleMap Compartment constructor option. export const makeAlias = (compartment, specifier) => freeze({ compartment, specifier }); // `resolveAll` pre-computes resolutions of all imports within the compartment // in which a module was loaded. const resolveAll = (imports, resolveHook, fullReferrerSpecifier) => { const resolvedImports = create(null); for (const importSpecifier of imports) { const fullSpecifier = resolveHook(importSpecifier, fullReferrerSpecifier); resolvedImports[importSpecifier] = fullSpecifier; } return freeze(resolvedImports); }; const loadModuleSource = ( compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, moduleSource, enqueueJob, selectImplementation, moduleLoads, importMeta, ) => { const { resolveHook, name: compartmentName } = weakmapGet( compartmentPrivateFields, compartment, ); const { imports } = moduleSource; if ( !isArray(imports) || arraySome(imports, specifier => typeof specifier !== 'string') ) { throw makeError( X`Invalid module source: 'imports' must be an array of strings, got ${imports} for module ${q(moduleSpecifier)} of compartment ${q(compartmentName)}`, ); } // resolve all imports relative to this referrer module. const resolvedImports = resolveAll(imports, resolveHook, moduleSpecifier); const moduleRecord = freeze({ compartment, moduleSource, moduleSpecifier, resolvedImports, importMeta, }); // Enqueue jobs to load this module's shallow dependencies. for (const fullSpecifier of values(resolvedImports)) { // Behold: recursion. // eslint-disable-next-line no-use-before-define enqueueJob(memoizedLoadWithErrorAnnotation, [ compartmentPrivateFields, moduleAliases, compartment, fullSpecifier, enqueueJob, selectImplementation, moduleLoads, ]); } return moduleRecord; }; function* loadWithoutErrorAnnotation( compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, enqueueJob, selectImplementation, moduleLoads, ) { const { importHook, importNowHook, moduleMap, moduleMapHook, moduleRecords, parentCompartment, } = weakmapGet(compartmentPrivateFields, compartment); if (mapHas(moduleRecords, moduleSpecifier)) { return mapGet(moduleRecords, moduleSpecifier); } // Follow moduleMap, or moduleMapHook if present. let moduleDescriptor = moduleMap[moduleSpecifier]; if (moduleDescriptor === undefined && moduleMapHook !== undefined) { moduleDescriptor = moduleMapHook(moduleSpecifier); } if (moduleDescriptor === undefined) { const moduleHook = selectImplementation(importHook, importNowHook); if (moduleHook === undefined) { const moduleHookName = selectImplementation( 'importHook', 'importNowHook', ); throw makeError( X`${b(moduleHookName)} needed to load module ${q( moduleSpecifier, )} in compartment ${q(compartment.name)}`, ); } moduleDescriptor = moduleHook(moduleSpecifier); // Uninitialized module namespaces throw if we attempt to coerce them into // promises. if (!weakmapHas(moduleAliases, moduleDescriptor)) { moduleDescriptor = yield moduleDescriptor; } } if (typeof moduleDescriptor === 'string') { // eslint-disable-next-line @endo/no-polymorphic-call throw makeError( X`Cannot map module ${q(moduleSpecifier)} to ${q( moduleDescriptor, )} in parent compartment, use {source} module descriptor`, TypeError, ); } else if (isObject(moduleDescriptor)) { // In this shim (and not in XS, and not in the standard we imagine), we // allow a module namespace object to stand in for a module descriptor that // describes its original {compartment, specifier} so that it can be used // to create a link. let aliasDescriptor = weakmapGet(moduleAliases, moduleDescriptor); if (aliasDescriptor !== undefined) { moduleDescriptor = aliasDescriptor; } if (moduleDescriptor.namespace !== undefined) { // { namespace: string, compartment?: Compartment } // Namespace module descriptors link to a module instance. if (typeof moduleDescriptor.namespace === 'string') { // The default compartment is the *parent*, not this child compartment. // This is a difference from the legacy {specifier, compartment} module // descriptor. const { compartment: aliasCompartment = parentCompartment, namespace: aliasSpecifier, } = moduleDescriptor; if ( !isObject(aliasCompartment) || !weakmapHas(compartmentPrivateFields, aliasCompartment) ) { throw makeError( X`Invalid compartment in module descriptor for specifier ${q(moduleSpecifier)} in compartment ${q(compartment.name)}`, ); } // Behold: recursion. // eslint-disable-next-line no-use-before-define const aliasRecord = yield memoizedLoadWithErrorAnnotation( compartmentPrivateFields, moduleAliases, aliasCompartment, aliasSpecifier, enqueueJob, selectImplementation, moduleLoads, ); mapSet(moduleRecords, moduleSpecifier, aliasRecord); return aliasRecord; } // All remaining objects must either be a module namespace, or be // promoted into a module namespace with a virtual module source. if (isObject(moduleDescriptor.namespace)) { const { namespace } = moduleDescriptor; // Brand-check SES shim module exports namespaces: aliasDescriptor = weakmapGet(moduleAliases, namespace); if (aliasDescriptor !== undefined) { moduleDescriptor = aliasDescriptor; // Fall through to processing the resulting {compartment, specifier} // alias. } else { // Promote an arbitrary object to a module namespace with a virtual // module source. // { namespace: Object } const exports = getOwnPropertyNames(namespace); /** @type {import('../types.js').VirtualModuleSource} */ const moduleSource = { imports: [], exports, execute(env) { for (const name of exports) { env[name] = namespace[name]; } }, }; const importMeta = undefined; const moduleRecord = loadModuleSource( compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, moduleSource, enqueueJob, selectImplementation, moduleLoads, importMeta, ); mapSet(moduleRecords, moduleSpecifier, moduleRecord); return moduleRecord; } } else { throw makeError( X`Invalid compartment in module descriptor for specifier ${q(moduleSpecifier)} in compartment ${q(compartment.name)}`, ); } } if (moduleDescriptor.source !== undefined) { // Module source descriptors create an instance from a module source. // The descriptor may contain the module source, or refer to a source // loaded in a particular compartment. if (typeof moduleDescriptor.source === 'string') { // { source: string, importMeta?, specifier?: string, compartment? } // A string source is the specifier for a different module source. // That source may come from this compartment's parent (default), or // from a specified compartment, and the specified compartment may be // this compartment to make a duplicate. const { source: loaderSpecifier, specifier: instanceSpecifier = moduleSpecifier, compartment: loaderCompartment = parentCompartment, importMeta = undefined, } = moduleDescriptor; // Induce the compartment, possibly a different compartment // to load a module source. // Behold: recursion. // eslint-disable-next-line no-use-before-define const loaderRecord = yield memoizedLoadWithErrorAnnotation( compartmentPrivateFields, moduleAliases, loaderCompartment, loaderSpecifier, enqueueJob, selectImplementation, moduleLoads, ); // Extract the source of the module from the loader compartment's // record. const { moduleSource } = loaderRecord; // Instantiate that source in our own compartment, possibly with a // different specifier for resolving its own imports. const moduleRecord = loadModuleSource( compartmentPrivateFields, moduleAliases, compartment, instanceSpecifier, moduleSource, enqueueJob, selectImplementation, moduleLoads, importMeta, ); mapSet(moduleRecords, moduleSpecifier, moduleRecord); return moduleRecord; } else { // { source: ModuleSource, importMeta?, specifier?: string } // We assume all non-string module sources are any of the supported // kinds of module source: PrecompiledModuleSource, // VirtualModuleSource, or a native ModuleSource. const { source: moduleSource, specifier: aliasSpecifier = moduleSpecifier, importMeta, } = moduleDescriptor; const aliasRecord = loadModuleSource( compartmentPrivateFields, moduleAliases, compartment, aliasSpecifier, moduleSource, enqueueJob, selectImplementation, moduleLoads, importMeta, ); mapSet(moduleRecords, moduleSpecifier, aliasRecord); return aliasRecord; } } if (moduleDescriptor.archive !== undefined) { // { archive: Archive, path: string } // We do not support this XS-native module descriptor. throw makeError( X`Unsupported archive module descriptor for specifier ${q(moduleSpecifier)} in compartment ${q(compartment.name)}`, ); } // { record, specifier?, compartment?, importMeta? } // A (legacy) module descriptor for when we find the module source (record) // but at a different specifier than requested. // Providing this {specifier, record} descriptor serves as an ergonomic // short-hand for stashing the record, returning a {compartment, specifier} // reference, bouncing the module hook, then producing the source (record) // when module hook receives the response specifier. if (moduleDescriptor.record !== undefined) { const { compartment: aliasCompartment = compartment, specifier: aliasSpecifier = moduleSpecifier, record: moduleSource, importMeta, } = moduleDescriptor; const aliasRecord = loadModuleSource( compartmentPrivateFields, moduleAliases, aliasCompartment, aliasSpecifier, moduleSource, enqueueJob, selectImplementation, moduleLoads, importMeta, ); mapSet(moduleRecords, moduleSpecifier, aliasRecord); mapSet(moduleRecords, aliasSpecifier, aliasRecord); return aliasRecord; } // { specifier: string, compartment: Compartment } // A (legacy) module descriptor that describes a link to a module instance // in a specified compartment. if ( moduleDescriptor.compartment !== undefined && moduleDescriptor.specifier !== undefined ) { if ( !isObject(moduleDescriptor.compartment) || !weakmapHas(compartmentPrivateFields, moduleDescriptor.compartment) || typeof moduleDescriptor.specifier !== 'string' ) { throw makeError( X`Invalid compartment in module descriptor for specifier ${q(moduleSpecifier)} in compartment ${q(compartment.name)}`, ); } // Behold: recursion. // eslint-disable-next-line no-use-before-define const aliasRecord = yield memoizedLoadWithErrorAnnotation( compartmentPrivateFields, moduleAliases, moduleDescriptor.compartment, moduleDescriptor.specifier, enqueueJob, selectImplementation, moduleLoads, ); mapSet(moduleRecords, moduleSpecifier, aliasRecord); return aliasRecord; } // A (legacy) behavior: If we do not recognize the module descriptor as a // module descriptor, we assume that it is a module source (record): const moduleSource = moduleDescriptor; const moduleRecord = loadModuleSource( compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, moduleSource, enqueueJob, selectImplementation, moduleLoads, ); // Memoize. mapSet(moduleRecords, moduleSpecifier, moduleRecord); return moduleRecord; } else { throw makeError( X`module descriptor must be a string or object for specifier ${q( moduleSpecifier, )} in compartment ${q(compartment.name)}`, ); } } const memoizedLoadWithErrorAnnotation = ( compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, enqueueJob, selectImplementation, moduleLoads, ) => { const { name: compartmentName } = weakmapGet( compartmentPrivateFields, compartment, ); // Prevent data-lock from recursion into branches visited in dependent loads. let compartmentLoading = mapGet(moduleLoads, compartment); if (compartmentLoading === undefined) { compartmentLoading = new Map(); mapSet(moduleLoads, compartment, compartmentLoading); } let moduleLoading = mapGet(compartmentLoading, moduleSpecifier); if (moduleLoading !== undefined) { return moduleLoading; } moduleLoading = selectImplementation(asyncTrampoline, syncTrampoline)( loadWithoutErrorAnnotation, [ compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, enqueueJob, selectImplementation, moduleLoads, ], error => { // eslint-disable-next-line @endo/no-polymorphic-call annotateError( error, X`${error.message}, loading ${q(moduleSpecifier)} in compartment ${q( compartmentName, )}`, ); throw error; }, ); mapSet(compartmentLoading, moduleSpecifier, moduleLoading); return moduleLoading; }; const asyncJobQueue = () => { /** @type {Set<Promise<undefined>>} */ const pendingJobs = new Set(); /** @type {Array<Error>} */ const errors = []; /** * Enqueues a job that starts immediately but won't be awaited until drainQueue is called. * * @template {any[]} T * @param {(...args: T)=>Promise<*>} func * @param {T} args */ const enqueueJob = (func, args) => { setAdd( pendingJobs, promiseThen(func(...args), noop, error => { arrayPush(errors, error); }), ); }; /** * Sequentially awaits pending jobs and returns an array of errors * * @returns {Promise<Array<Error>>} */ const drainQueue = async () => { await null; for (const job of pendingJobs) { // eslint-disable-next-line no-await-in-loop await job; } return errors; }; return { enqueueJob, drainQueue }; }; /** * @param {object} options * @param {Array<Error>} options.errors * @param {string} options.errorPrefix */ const throwAggregateError = ({ errors, errorPrefix }) => { // Throw an aggregate error if there were any errors. if (errors.length > 0) { const verbose = getenv('COMPARTMENT_LOAD_ERRORS', '', ['verbose']) === 'verbose'; throw TypeError( `${errorPrefix} (${errors.length} underlying failures: ${arrayJoin( arrayMap(errors, error => error.message + (verbose ? error.stack : '')), ', ', )}`, ); } }; const preferSync = (_asyncImpl, syncImpl) => syncImpl; const preferAsync = (asyncImpl, _syncImpl) => asyncImpl; /* * `load` asynchronously gathers the module records for a module and its * transitive dependencies. * The module records refer to each other by a reference to the dependency's * compartment and the specifier of the module within its own compartment. * This graph is then ready to be synchronously linked and executed. */ export const load = async ( compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, ) => { const { name: compartmentName } = weakmapGet( compartmentPrivateFields, compartment, ); /** @type {Map<object, Map<string, Promise<Record<any, any>>>>} */ const moduleLoads = new Map(); const { enqueueJob, drainQueue } = asyncJobQueue(); enqueueJob(memoizedLoadWithErrorAnnotation, [ compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, enqueueJob, preferAsync, moduleLoads, ]); // Drain pending jobs queue and throw an aggregate error const errors = await drainQueue(); throwAggregateError({ errors, errorPrefix: `Failed to load module ${q(moduleSpecifier)} in package ${q( compartmentName, )}`, }); }; /* * `loadNow` synchronously gathers the module records for a specified module * and its transitive dependencies. * The module records refer to each other by a reference to the dependency's * compartment and the specifier of the module within its own compartment. * This graph is then ready to be synchronously linked and executed. */ export const loadNow = ( compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, ) => { const { name: compartmentName } = weakmapGet( compartmentPrivateFields, compartment, ); /** @type {Map<object, Map<string, Promise<Record<any, any>>>>} */ const moduleLoads = new Map(); /** @type {Array<Error>} */ const errors = []; const enqueueJob = (func, args) => { try { func(...args); } catch (error) { arrayPush(errors, error); } }; enqueueJob(memoizedLoadWithErrorAnnotation, [ compartmentPrivateFields, moduleAliases, compartment, moduleSpecifier, enqueueJob, preferSync, moduleLoads, ]); throwAggregateError({ errors, errorPrefix: `Failed to load module ${q(moduleSpecifier)} in package ${q( compartmentName, )}`, }); };