@validation-engineering/cordis-loader 0.0.0-stage → 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/LICENSE +22 -0
- package/NOTICE +49 -0
- package/README.md +146 -2
- package/artifact.js +80 -0
- package/config.js +102 -0
- package/domain-entry.js +85 -0
- package/index.d.ts +83 -0
- package/index.js +384 -0
- package/module-graph.js +150 -0
- package/module-host.js +51 -0
- package/package.json +54 -4
- package/process-entry.js +10 -0
- package/process.d.ts +42 -0
- package/process.js +155 -0
- package/rust-module-plugin.d.ts +35 -0
- package/rust-module-plugin.js +195 -0
- package/rust-module-state.js +66 -0
- package/rust-module.d.ts +74 -0
- package/rust-module.js +269 -0
- package/worker-entry.js +7 -0
- package/worker.d.ts +53 -0
- package/worker.js +233 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cordis-verus contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/NOTICE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
This is an independent Rust reimplementation of a subset of Cordis semantics.
|
|
2
|
+
It is not an official Cordis, DeepSeek, or Verus distribution.
|
|
3
|
+
|
|
4
|
+
Design and behavior references:
|
|
5
|
+
- Cordis, https://github.com/cordiverse/cordis (MIT)
|
|
6
|
+
- DeepSeek Harness, https://github.com/deepseek-ai/deepseek-harness (MIT)
|
|
7
|
+
- A Programming Paradigm for Spatiotemporal Composability,
|
|
8
|
+
https://arxiv.org/abs/2608.25512v1 (https://github.com/validation-engineering/cordis-verus/blob/main/reference/README.md)
|
|
9
|
+
|
|
10
|
+
The ignored upstream checkouts retain their own license and copyright files.
|
|
11
|
+
The reference paper is not covered by this project's MIT license.
|
|
12
|
+
Verus binaries and vstd retain their upstream licenses. Binary archives are
|
|
13
|
+
downloaded into the ignored .tools directory. Project-managed toolchain archives
|
|
14
|
+
preserve original upstream bytes with a source/notices companion; see
|
|
15
|
+
https://github.com/validation-engineering/cordis-verus/blob/main/docs/toolchains/README.md.
|
|
16
|
+
They are separate from Cordis runtime releases.
|
|
17
|
+
|
|
18
|
+
The experimental JavaScript compatibility package adapts language-layer code
|
|
19
|
+
from Cordis (Copyright (c) 2021-present Shigma, MIT). The original MIT notice
|
|
20
|
+
is preserved in packages/compat-cordis/LICENSE.upstream. It does not embed the
|
|
21
|
+
upstream Fiber lifecycle scheduler. Node-API dependencies retain their own licenses.
|
|
22
|
+
|
|
23
|
+
The compatibility logger in packages/compat-cordis/logger.js is adapted from
|
|
24
|
+
Cordis packages/core/src/logger.ts at the locked Cordis revision. Its formatting,
|
|
25
|
+
exporter and traceable-caller behavior are ordinary JavaScript, outside the
|
|
26
|
+
Verus proof boundary, and retain the same MIT attribution above.
|
|
27
|
+
|
|
28
|
+
Windows configuration persistence uses atomicwrites 0.4.4
|
|
29
|
+
(https://github.com/untitaker/rust-atomicwrites), under the MIT license:
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2015 Markus Unterwaditzer
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
34
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
35
|
+
the Software without restriction, including without limitation the rights to
|
|
36
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
37
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
38
|
+
so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Native Cordis JSON Loader
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This experimental package loads real JS Cordis plugins through `compat-cordis` and the Rust native driver. It adds a **JSON configuration API** and an explicit **whole-Worker code replacement API**. It does not yet implement the upstream Loader/Include classes or Harness configuration language. A Loader can use either native Context profile; the Worker bootstrap selects the Cordis profile.
|
|
4
|
+
|
|
5
|
+
Use the matching `@validation-engineering/cordis` package, which includes the precompiled native addon. No compiler or additional runtime dependencies are required beyond that package and Node 22.22+. Source contributors can rebuild it with `npm run build:native`.
|
|
6
|
+
|
|
7
|
+
## Load a configuration in the current environment
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { Context } from '@validation-engineering/cordis';
|
|
11
|
+
import { Loader } from '@validation-engineering/cordis-loader';
|
|
12
|
+
|
|
13
|
+
const context = new Context();
|
|
14
|
+
const loader = new Loader(context);
|
|
15
|
+
await loader.loadFile('./cordis.json');
|
|
16
|
+
console.log(loader.diagnostics());
|
|
17
|
+
await loader.update('greeting', { config: { prefix: 'Welcome' } });
|
|
18
|
+
await loader.dispose();
|
|
19
|
+
await context.dispose();
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Start the program with `--import @validation-engineering/cordis/register` when plugin sources import the original `cordis` package name. The registered facade is still the only lifecycle implementation.
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
"id": "greeting",
|
|
28
|
+
"name": "./plugins/greeting.mjs",
|
|
29
|
+
"config": { "prefix": "Hello" }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "tools",
|
|
33
|
+
"include": "./tools.json",
|
|
34
|
+
"isolate": ["tools"]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
An input is an entry array or `{ "entries": [...] }`. Every entry has an explicit stable `id`. Module and include paths resolve relative to the configuration file that contains them. `include` adds a group; `group: true` with `entries` creates an inline group. Nested entry IDs use `/`, for example `tools/search`. IDs may contain letters, digits, `_`, `-` and `.`. Include cycles, duplicate IDs, unsupported fields and invalid JSON values fail before the active graph is stopped.
|
|
40
|
+
|
|
41
|
+
Supported options are `id`, `name`, `config`, `disabled`, `group`, `entries`, `include`, `inject`, `isolate` and `intercept`. `inject` accepts service names or a name-to-config mapping. `isolate: ["service"]` creates an entry-specific realm; `isolate: {"service": "shared-label"}` intentionally shares that label between entries in this Loader. Groups own their child fibers. Disabled groups retain entry records while skipping module imports and activation for their descendants.
|
|
42
|
+
|
|
43
|
+
Entries returned by `resolve()` keep their object identity across successful updates and restoration. A persistent Loader owner contains the configured tree. Updates compare each stable ID's canonical module URL, prepared plugin object identity and optional revision, JSON config, injection, isolation, interception, source base URL and parent. Unchanged fibers remain mounted; changed entries and descendants of changed parents are retired and recreated. Adding or removing children does not restart an unchanged group. JSON object key order and sibling reordering do not restart instances; sibling order governs newly mounted entries and diagnostics, while retained instances keep their original effect registration order.
|
|
44
|
+
|
|
45
|
+
A preserved consumer can still run cleanup and setup again when its dependency changes: the shared native driver owns that transition, and the consumer keeps its Fiber ID while its episode changes. A group's dependency restart recreates its owned children from the latest committed recipe. Changing a group's isolation/injection/interception replaces its entire subtree. Config changes currently replace the changed plugin rather than invoke upstream in-place update hooks. Explicit entry inject metadata uses a forwarding plugin callback and is not an exact upstream registry-identity implementation. The application Context and unrelated plugins are not disposed by `loader.dispose()`.
|
|
46
|
+
|
|
47
|
+
### Readiness, serialization and restoration
|
|
48
|
+
|
|
49
|
+
`await ctx.plugin()` can settle Pending. The Loader therefore checks every enabled configured entry after settlement. By default a missing dependency rejects activation with `DEPENDENCIES_UNAVAILABLE` and structured `details.entries` diagnostics. `allowPending: true` accepts a partially available tree. Diagnostics cover configured entries; arbitrary plugin-created children retain their own lifecycle API.
|
|
50
|
+
|
|
51
|
+
External `apply`, `loadFile`, `update`, `setEnabled`, `reload` and `dispose` calls enter the runtime domain's shared mutation queue at admission. All Loaders over the same Context domain share that queue with external Fiber updates, restarts, disposal and domain shutdown. Preparing a candidate is part of the transaction, so another Loader cannot activate a competing revision while its modules are being prepared. Independent Context domains keep separate queues.
|
|
52
|
+
|
|
53
|
+
Plugin setup can immediately create and await `ctx.plugin()` children; effect registration and native completion continue during a transaction. Loader retirement, recovery and cleanup use scoped transaction steps, without queueing behind their own operation. Calling a Loader mutation from an active lifecycle callback or another transaction, including a different Loader's callback or module preparation, is rejected as `REENTRANT_MUTATION`. This also applies when the requested close is already queued; joining that Promise would wait on the action currently blocking it.
|
|
54
|
+
|
|
55
|
+
`loader.dispose()` closes only that Loader and joins repeated external requests. `context.dispose()` closes admission for new domain revisions as soon as shutdown is requested, waits for previously admitted transactions, and then drains the graph. An admitted transaction can still finish plugin setup and cleanup. A cleanup failure anywhere in the domain blocks subsequent ordinary revisions; explicit cleanup retry and disposal remain available. This does not make module evaluation or external side effects atomic, add configuration write-back, or invalidate Node's module caches.
|
|
56
|
+
|
|
57
|
+
The Loader imports modules and validates candidate JSON inputs before disturbing the old tree. Runtime plugin schema validation still occurs when the replacement is mounted. It retains the old module objects and an immutable JSON recipe. Each activation gets fresh configuration objects. The changed old subtrees and their dependent cleanup drain before replacements mount. If candidate setup fails, every candidate-created fiber and its children must be removed before the replaced part of the old recipe can restart. Unaffected siblings and parents remain mounted. Group controllers are restored to the old recipe as well, so a later dependency notification cannot reactivate rejected configuration. A retained consumer that failed while observing the candidate is explicitly restarted against the restored dependencies. A cleanup failure leaves the Loader `blocked`; it does not activate the old recipe or a queued replacement. After fixing the external cause, `retryCleanup()` retries failed inverses and drains the whole Loader, including retained siblings, returning to an empty state. The caller can then explicitly load a configuration. Successful inverses are not repeated.
|
|
58
|
+
|
|
59
|
+
`RELOAD_FAILED` with `details.restored: true` means the replaced part of the old recipe was instantiated again; replaced live objects do not survive. Unchanged entries can keep their instances. Module evaluation, arbitrary global state and external file/network/database effects are not rolled back.
|
|
60
|
+
|
|
61
|
+
`loader.reload()` re-reads its last file and included JSON files, then reconciles the changed subtrees. With unchanged configuration and factory identities, it does not remount the graph. After a programmatic `update()` the source is an in-memory recipe; subsequent `reload()` uses that recipe and asks the ModuleHost to prepare its factories again. There is no implicit write-back to the original configuration files and no background file watcher.
|
|
62
|
+
|
|
63
|
+
## DeepSeek Harness application integration
|
|
64
|
+
|
|
65
|
+
The pinned official Loader/Include, ConfigEditor and in-process HMR bridges are
|
|
66
|
+
maintained and tested by the companion Harness application. Starting with Loader
|
|
67
|
+
0.2.0, import those APIs from `@validation-engineering/harness-verus/official-loader-api`.
|
|
68
|
+
The former `@validation-engineering/cordis-loader/harness` entry belongs to the
|
|
69
|
+
published 0.1.0 release and is no longer exported by this package.
|
|
70
|
+
|
|
71
|
+
The runtime still provides the shared domain transaction, callback isolation,
|
|
72
|
+
cleanup and JSON Loader primitives. Application-specific method interception,
|
|
73
|
+
module-cache recovery and official version checks live with the application.
|
|
74
|
+
See [package boundaries](../../docs/package-boundaries.md) for the migration.
|
|
75
|
+
|
|
76
|
+
## Replace code in a fresh Worker
|
|
77
|
+
|
|
78
|
+
`ModuleHost` imports canonical file URLs. It **does not invalidate Node's ESM or CommonJS caches**. Changing a source file and calling `loader.reload()` is not code HMR. `ModuleHost.reset()` explicitly rejects with `DOMAIN_RESTART_REQUIRED`. A file edit at an already imported URL does not change the cached factory identity and is not a request for code replacement.
|
|
79
|
+
|
|
80
|
+
An embedding host can supply an adapter for **already prepared factories**:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { ModuleHost } from '@validation-engineering/cordis-loader';
|
|
84
|
+
|
|
85
|
+
const moduleHost = new ModuleHost({
|
|
86
|
+
loadModule: async canonicalURL => preparedFactories.get(canonicalURL),
|
|
87
|
+
});
|
|
88
|
+
// Each value is { plugin, namespace?, revision?: string | number }.
|
|
89
|
+
const loader = new Loader(context, { moduleHost });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Resolution still requires an explicit existing file path and applies `rootDirectory` confinement. The adapter replaces only module loading; it does not bypass JSON validation or lifecycle cleanup. It must return the same plugin object and revision while the executable factory is unchanged. A changed identity or revision requests replacement of entries using that module. Revision is a comparison token, not a claim that Node's dependency cache was reset. Publish immutable factory snapshots: the retained old plugin object must remain executable for rollback. Building source, evaluating it in another environment, deciding the dependency closure and maintaining those factory snapshots remain the embedding host's responsibility. Use `WorkerDomain` or `ProcessDomain` when an ESM/CommonJS environment and its transitive dependencies must actually be replaced.
|
|
93
|
+
|
|
94
|
+
Use a `WorkerDomain` for real code replacement, including changed transitive imports:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { WorkerDomain } from '@validation-engineering/cordis-loader/worker';
|
|
98
|
+
|
|
99
|
+
const domain = new WorkerDomain({ entry: 'cordis.json' });
|
|
100
|
+
await domain.load('./plugin-project');
|
|
101
|
+
console.log(await domain.call('greeting', 'hello', 'Alice'));
|
|
102
|
+
// Build/publish the candidate project directory first.
|
|
103
|
+
await domain.reload('./plugin-project');
|
|
104
|
+
await domain.dispose();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Each load captures the local project directory and its dependencies/assets into a retained artifact, with content hashes and permissions. Each Worker receives a separate launch copy. Mutating launch files cannot change the retained recovery recipe. A new Worker owns a new Node environment, native Driver and real module cache. No query suffix is used to simulate dependency-graph reload.
|
|
108
|
+
|
|
109
|
+
Replacement follows this order:
|
|
110
|
+
|
|
111
|
+
1. Capture the candidate artifact while the old Worker is available.
|
|
112
|
+
2. Stop new requests and drain admitted service calls, then normally dispose the old native graph and exit the Worker.
|
|
113
|
+
3. Start the candidate Worker and require configured entries to become ready.
|
|
114
|
+
4. On startup failure, normally drain and exit that candidate; only then restart the retained old artifact in another fresh Worker.
|
|
115
|
+
|
|
116
|
+
A cleanup failure or unknown timeout blocks replacement. An unexpected Worker exit is `abandoned`, not a successful shutdown. `abandon()` explicitly terminates remaining Workers and returns `{ abandoned: true, cleanupConfirmed: false }`; it never automatically restores the old artifact. The default operation timeout is 30 seconds and is only a failure detector, not a cancellation or cleanup proof.
|
|
117
|
+
|
|
118
|
+
Objects/functions stay ordinary JS objects **inside** each Worker. `WorkerDomain.call(service, method, ...args)` is an explicit asynchronous JSON boundary between environments. It has no transparent callback/stream/object transport. A Worker is not an operating-system process or a security sandbox; a native crash can affect the whole process.
|
|
119
|
+
|
|
120
|
+
### Replace an OS process
|
|
121
|
+
|
|
122
|
+
Use `ProcessDomain` from `@validation-engineering/cordis-loader/process` for application
|
|
123
|
+
native addons or a whole application process. It has the same captured-artifact
|
|
124
|
+
load/reload/call workflow and adds a read-only PID. A successful replacement needs
|
|
125
|
+
both a cleanup acknowledgement and exit code zero; a timeout or crash is not
|
|
126
|
+
normal cleanup. An optional trusted `hostModule` can host the official Harness
|
|
127
|
+
without creating another Context. See the [process contract and example](../../docs/module-graph-reloads.md#processdomain-application-native-addons-and-external-hosts)
|
|
128
|
+
for adapter, provenance and failure boundaries. The companion Harness exposes an
|
|
129
|
+
opt-in [official Web supervisor](https://github.com/validation-engineering/cordis-harness/blob/main/docs/process-supervisor.md).
|
|
130
|
+
|
|
131
|
+
### Artifact contract
|
|
132
|
+
|
|
133
|
+
Publish a complete, stable input directory before capture. Include all relative imports, installed dependencies, resources and configuration. The directory must not change during capture. `.git` is excluded; symlinks, special files and artifacts exceeding configured limits are rejected. Defaults are 256 MiB and 20,000 files. Materialize symlinked package dependencies before capture. The facade/addon and Node runtime come from the host installation, not the captured project.
|
|
134
|
+
|
|
135
|
+
Keep persistent writable data outside the captured directory and pass its path explicitly in JSON config. Per-launch writable changes are discarded after shutdown. Entry modules and all JSON Include files must be inside the artifact. Arbitrary imports made by plugin code can still escape that directory; they are not a captured dependency or a sandboxed operation. Complete dependency discovery, package installation and signed release manifests remain future work.
|
|
136
|
+
|
|
137
|
+
## Current compatibility boundary
|
|
138
|
+
|
|
139
|
+
This package supports explicit file module paths and Node's normal execution of built JS/CJS. Bare npm plugin entry names, source TS transformation, YAML/custom tags, JS expressions, upstream Loader event/API compatibility, automatic config write-back, transparent in-process code HMR and Harness lazy/volatile configuration are not implemented. Plugin dependencies may use Node's normal package imports once installed in the project. A `cordis` import is routed by the existing facade bootstrap; embedded/private Cordis copies are not detected by this package.
|
|
140
|
+
|
|
141
|
+
The implementation is ordinary JS host code. Its tests establish the listed behavior; they do not add Verus proofs for module loading, arbitrary plugin callbacks, Worker teardown or filesystem operations.
|
|
142
|
+
|
|
143
|
+
Run `node --test --test-timeout=30000 tests/node-loader/*.test.mjs` from the repository root. The tests execute actual plugin files, nested JSON Include and isolated groups, candidate children, explicit readiness checks, code changes to a transitive import, snapshot recovery, cleanup failure, queued/reentrant updates, Worker crash and TypeScript usage.
|
|
144
|
+
|
|
145
|
+
## Local distribution check
|
|
146
|
+
|
|
147
|
+
`node scripts/check-npm-package.mjs --offline` stages the facade, Loader and Harness profile packages, includes the freshly recorded host-native addon and notices, runs `npm pack --ignore-scripts`, and installs the tarballs in a separate temporary project. It verifies that no workspace path or symlink supplies the packages, then runs native, Loader, Worker, Process (including an application native addon) and Harness profile examples from that installation. The report and local tarballs are written to `target/release-artifacts/npm`. Only the tested OS/architecture/Node combination is recorded as validated; this check uploads nothing and does not establish a prebuilt distribution matrix or registry publish eligibility.
|
package/artifact.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdir, mkdtemp, readdir, readFile, rm, writeFile, chmod } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve, relative, isAbsolute } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { LoaderError } from './config.js';
|
|
6
|
+
|
|
7
|
+
const managedLaunches = new Set();
|
|
8
|
+
/** Release only a private directory allocated by Artifact.launch in this process. */
|
|
9
|
+
export async function releaseLaunch(directory) {
|
|
10
|
+
if (!managedLaunches.has(directory)) throw new LoaderError('ARTIFACT_CLEANUP_TARGET', 'Only a registered private artifact launch may be released');
|
|
11
|
+
await rm(directory, { recursive: true, force: true });
|
|
12
|
+
managedLaunches.delete(directory);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Each isolated environment gets a private copy, never the retained restoration recipe. */
|
|
16
|
+
export class Artifact {
|
|
17
|
+
static async capture(directory, options = {}) {
|
|
18
|
+
const source = resolve(directory);
|
|
19
|
+
if (!(await lstat(source)).isDirectory()) throw new LoaderError('ARTIFACT_DIRECTORY', 'Artifact source must be a directory');
|
|
20
|
+
const storage = await mkdtemp(join(tmpdir(), 'cordis-artifact-'));
|
|
21
|
+
const root = join(storage, 'project');
|
|
22
|
+
await mkdir(root);
|
|
23
|
+
const manifest = [];
|
|
24
|
+
const directories = [];
|
|
25
|
+
let bytes = 0;
|
|
26
|
+
const maxBytes = options.maxBytes ?? 256 * 1024 * 1024;
|
|
27
|
+
const maxFiles = options.maxFiles ?? 20000;
|
|
28
|
+
async function copy(folder = '') {
|
|
29
|
+
const entries = (await readdir(join(source, folder), { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.name === '.git') continue;
|
|
32
|
+
const name = join(folder, entry.name);
|
|
33
|
+
const stat = await lstat(join(source, name));
|
|
34
|
+
if (stat.isSymbolicLink()) throw new LoaderError('ARTIFACT_SYMLINK', `Artifact symlinks are not supported: ${name}; materialize dependencies first`);
|
|
35
|
+
if (stat.isDirectory()) { directories.push(name); await mkdir(join(root, name)); await copy(name); continue; }
|
|
36
|
+
if (!stat.isFile()) throw new LoaderError('ARTIFACT_FILE_TYPE', `Unsupported artifact file ${name}`);
|
|
37
|
+
if (bytes + stat.size > maxBytes || manifest.length >= maxFiles) throw new LoaderError('ARTIFACT_LIMIT', 'Artifact exceeds configured byte/file limit');
|
|
38
|
+
const content = await readFile(join(source, name));
|
|
39
|
+
bytes += content.length;
|
|
40
|
+
if (bytes > maxBytes) throw new LoaderError('ARTIFACT_LIMIT', 'Artifact changed beyond its byte limit during capture');
|
|
41
|
+
const mode = stat.mode & 0o777;
|
|
42
|
+
await writeFile(join(root, name), content);
|
|
43
|
+
await chmod(join(root, name), mode);
|
|
44
|
+
manifest.push({ name, mode, bytes: content.length, sha256: createHash('sha256').update(content).digest('hex') });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await copy();
|
|
49
|
+
const digest = createHash('sha256').update(JSON.stringify({ files: manifest, directories })).digest('hex');
|
|
50
|
+
return new Artifact(storage, root, source, manifest, directories, digest);
|
|
51
|
+
} catch (error) { await rm(storage, { recursive: true, force: true }); throw error; }
|
|
52
|
+
}
|
|
53
|
+
constructor(storage, root, source, manifest, directories, digest) {
|
|
54
|
+
this.storage = storage; this.root = root; this.source = source;
|
|
55
|
+
this.manifest = Object.freeze(manifest.map(item => Object.freeze(item)));
|
|
56
|
+
this.directories = Object.freeze(directories);
|
|
57
|
+
this.digest = digest; this.closed = false;
|
|
58
|
+
}
|
|
59
|
+
async launch(entry) {
|
|
60
|
+
if (this.closed) throw new LoaderError('ARTIFACT_CLOSED', 'The retained artifact has been released');
|
|
61
|
+
const location = relative(this.root, resolve(this.root, entry));
|
|
62
|
+
if (!location || location === '..' || location.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(location)) throw new LoaderError('ARTIFACT_ENTRY', 'entry must be a file inside the artifact');
|
|
63
|
+
const directory = await mkdtemp(join(tmpdir(), 'cordis-worker-'));
|
|
64
|
+
managedLaunches.add(directory);
|
|
65
|
+
try {
|
|
66
|
+
// Only captured entries are copied; later additions to retained storage
|
|
67
|
+
// cannot enter an artifact under its original digest.
|
|
68
|
+
for (const name of this.directories) await mkdir(join(directory, name));
|
|
69
|
+
for (const item of this.manifest) {
|
|
70
|
+
const content = await readFile(join(this.root, item.name));
|
|
71
|
+
if (createHash('sha256').update(content).digest('hex') !== item.sha256) throw new LoaderError('ARTIFACT_CHANGED', `Retained artifact changed: ${item.name}`);
|
|
72
|
+
await writeFile(join(directory, item.name), content);
|
|
73
|
+
await chmod(join(directory, item.name), item.mode);
|
|
74
|
+
}
|
|
75
|
+
return { directory, entry: location };
|
|
76
|
+
} catch (error) { await releaseLaunch(directory); throw error; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async dispose() { if (!this.closed) { await rm(this.storage, { recursive: true, force: true }); this.closed = true; } }
|
|
80
|
+
}
|
package/config.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, resolve, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export class LoaderError extends Error {
|
|
6
|
+
constructor(code, message, details = {}, cause) {
|
|
7
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
8
|
+
this.name = 'LoaderError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function jsonValue(value, path = '$', seen = new Set()) {
|
|
15
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
16
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
17
|
+
if (typeof value !== 'object' || seen.has(value)) throw new LoaderError('INVALID_CONFIG', `${path} must contain finite JSON values only`);
|
|
18
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
19
|
+
throw new LoaderError('INVALID_CONFIG', `${path} must be a plain JSON object`);
|
|
20
|
+
}
|
|
21
|
+
seen.add(value);
|
|
22
|
+
const result = Array.isArray(value) ? value.map((item, index) => jsonValue(item, `${path}[${index}]`, seen)) : Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonValue(item, `${path}.${key}`, seen)]));
|
|
23
|
+
seen.delete(value);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function freezeJSON(value) {
|
|
28
|
+
if (value && typeof value === 'object') { for (const item of Object.values(value)) freezeJSON(item); Object.freeze(value); }
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function fileURL(input, baseURL = pathToFileURL(`${process.cwd()}/`).href) {
|
|
33
|
+
if (input instanceof URL) input = input.href;
|
|
34
|
+
if (typeof input !== 'string' || !input) throw new LoaderError('INVALID_PATH', 'Expected a file path or file URL');
|
|
35
|
+
const url = input.startsWith('file:') ? new URL(input) : isAbsolute(input) ? pathToFileURL(input) : new URL(input, baseURL);
|
|
36
|
+
if (url.protocol !== 'file:' || url.search || url.hash) throw new LoaderError('INVALID_PATH', 'Only ordinary file paths without query or fragment are supported');
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function object(value, at) {
|
|
41
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new LoaderError('INVALID_CONFIG', `${at} must be an object`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function readTree(input, options = {}) {
|
|
45
|
+
const baseURL = options.baseURL ?? pathToFileURL(`${resolve('.')}/`).href;
|
|
46
|
+
const root = options.rootDirectory ? await realpath(options.rootDirectory) : undefined;
|
|
47
|
+
const files = new Set();
|
|
48
|
+
const ids = new Set();
|
|
49
|
+
const stack = [];
|
|
50
|
+
async function fromFile(filename, parent, base) {
|
|
51
|
+
const url = fileURL(filename, base);
|
|
52
|
+
const canonicalPath = await realpath(fileURLToPath(url));
|
|
53
|
+
if (root) {
|
|
54
|
+
const location = relative(root, canonicalPath);
|
|
55
|
+
if (location === '..' || location.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(location)) throw new LoaderError('CONFIG_OUTSIDE_ARTIFACT', `${canonicalPath} is outside artifact ${root}`);
|
|
56
|
+
}
|
|
57
|
+
const canonical = pathToFileURL(canonicalPath).href;
|
|
58
|
+
if (stack.includes(canonical)) throw new LoaderError('INCLUDE_CYCLE', `Include cycle: ${[...stack, canonical].join(' -> ')}`);
|
|
59
|
+
if (stack.length >= 64) throw new LoaderError('INCLUDE_DEPTH', 'Include nesting exceeds 64 files');
|
|
60
|
+
stack.push(canonical);
|
|
61
|
+
files.add(canonical);
|
|
62
|
+
let raw;
|
|
63
|
+
try { raw = JSON.parse(await readFile(new URL(canonical), 'utf8')); }
|
|
64
|
+
catch (cause) { throw new LoaderError('CONFIG_READ', `Cannot read JSON configuration ${canonical}`, { file: canonical }, cause); }
|
|
65
|
+
try { return await entries(raw, parent, canonical); } finally { stack.pop(); }
|
|
66
|
+
}
|
|
67
|
+
async function entries(tree, parent, base) {
|
|
68
|
+
if (!Array.isArray(tree)) {
|
|
69
|
+
object(tree, 'configuration');
|
|
70
|
+
if (Object.keys(tree).some(key => key !== 'entries')) throw new LoaderError('INVALID_CONFIG', 'The config object supports only entries');
|
|
71
|
+
tree = tree.entries;
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(tree)) throw new LoaderError('INVALID_CONFIG', 'Configuration must be an entries array');
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const raw of tree) {
|
|
76
|
+
object(raw, 'entry');
|
|
77
|
+
const item = freezeJSON(jsonValue(raw));
|
|
78
|
+
const allowed = ['id', 'name', 'config', 'inject', 'isolate', 'intercept', 'disabled', 'group', 'entries', 'include'];
|
|
79
|
+
const unknown = Object.keys(item).filter(key => !allowed.includes(key));
|
|
80
|
+
if (unknown.length) throw new LoaderError('INVALID_CONFIG', `Unknown entry fields: ${unknown.join(', ')}`);
|
|
81
|
+
if (typeof item.id !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(item.id)) throw new LoaderError('INVALID_CONFIG', 'Every entry needs a stable id containing letters, digits, _, - or .');
|
|
82
|
+
const id = parent ? `${parent}/${item.id}` : item.id;
|
|
83
|
+
if (ids.has(id)) throw new LoaderError('DUPLICATE_ENTRY', `Duplicate entry id ${id}`);
|
|
84
|
+
ids.add(id);
|
|
85
|
+
if (item.disabled !== undefined && typeof item.disabled !== 'boolean') throw new LoaderError('INVALID_CONFIG', `${id}.disabled must be boolean`);
|
|
86
|
+
if (item.group !== undefined && typeof item.group !== 'boolean') throw new LoaderError('INVALID_CONFIG', `${id}.group must be boolean`);
|
|
87
|
+
if (item.inject !== undefined && !(Array.isArray(item.inject) ? item.inject.every(name => typeof name === 'string' && name.length) : item.inject && typeof item.inject === 'object')) throw new LoaderError('INVALID_CONFIG', `${id}.inject must be service names or a config object`);
|
|
88
|
+
if (item.intercept !== undefined) object(item.intercept, `${id}.intercept`);
|
|
89
|
+
if (item.isolate !== undefined && !(Array.isArray(item.isolate) ? item.isolate.every(name => typeof name === 'string' && name.length) : item.isolate && typeof item.isolate === 'object' && Object.values(item.isolate).every(label => label === true || typeof label === 'string'))) throw new LoaderError('INVALID_CONFIG', `${id}.isolate must be service names or name-to-label mapping`);
|
|
90
|
+
const group = item.group === true || item.include !== undefined;
|
|
91
|
+
if (group) {
|
|
92
|
+
if (item.name !== undefined || item.config !== undefined) throw new LoaderError('INVALID_CONFIG', `${id}: a group cannot have name or config`);
|
|
93
|
+
if (item.include !== undefined && item.entries !== undefined) throw new LoaderError('INVALID_CONFIG', `${id}: include and entries are mutually exclusive`);
|
|
94
|
+
} else if (typeof item.name !== 'string' || !item.name || item.entries !== undefined) throw new LoaderError('INVALID_CONFIG', `${id}: a plugin requires name; children require group: true`);
|
|
95
|
+
const children = group ? item.include !== undefined ? await fromFile(item.include, id, base) : await entries(item.entries ?? [], id, base) : [];
|
|
96
|
+
result.push({ id, options: item, baseURL: base, group, children });
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
const tree = typeof input === 'string' || input instanceof URL ? await fromFile(input, '', baseURL) : await entries(jsonValue(input), '', baseURL);
|
|
101
|
+
return { tree, files: [...files] };
|
|
102
|
+
}
|
package/domain-entry.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { LoaderError, jsonValue } from './config.js';
|
|
5
|
+
import { ModuleGraph } from './module-graph.js';
|
|
6
|
+
|
|
7
|
+
const serialize = (error, depth = 0) => ({ name: error?.name ?? 'Error', message: error?.message ?? String(error), code: error?.code, details: error?.details, cause: depth < 8 && error?.cause ? serialize(error.cause, depth + 1) : undefined });
|
|
8
|
+
|
|
9
|
+
/** One runtime per child. The transport has no lifecycle scheduler of its own. */
|
|
10
|
+
export function startEndpoint(channel, launch, options = {}) {
|
|
11
|
+
let graph;
|
|
12
|
+
let host, startupError, closing = false, shutdown;
|
|
13
|
+
const calls = new Set();
|
|
14
|
+
const startup = (async () => {
|
|
15
|
+
if (options.hostModule) {
|
|
16
|
+
const { url, sha256 } = options.hostModule;
|
|
17
|
+
const verify = async () => {
|
|
18
|
+
if (createHash('sha256').update(await readFile(new URL(url))).digest('hex') !== sha256) throw new LoaderError('HOST_CHANGED', 'The trusted host module changed after it was pinned');
|
|
19
|
+
};
|
|
20
|
+
await verify();
|
|
21
|
+
const module = await import(url);
|
|
22
|
+
await verify();
|
|
23
|
+
if (typeof module.createHost !== 'function') throw new LoaderError('HOST_CONTRACT', 'Trusted host must export synchronous createHost(options)');
|
|
24
|
+
graph = new ModuleGraph(launch.directory, options).install();
|
|
25
|
+
const candidate = module.createHost({ directory: launch.directory, entry: launch.entry, allowPending: launch.allowPending });
|
|
26
|
+
if (!candidate || typeof candidate.then === 'function' || typeof candidate.ready?.then !== 'function' || ['call', 'diagnostics', 'close'].some(key => typeof candidate[key] !== 'function')) throw new LoaderError('HOST_CONTRACT', 'createHost must immediately expose call(), diagnostics(), close(), and a ready Promise');
|
|
27
|
+
host = candidate;
|
|
28
|
+
await host.ready;
|
|
29
|
+
} else {
|
|
30
|
+
await import('@validation-engineering/cordis/register');
|
|
31
|
+
graph = new ModuleGraph(launch.directory, options).install();
|
|
32
|
+
const { Context } = await import('@validation-engineering/cordis');
|
|
33
|
+
const { Loader, ModuleHost } = await import('./index.js');
|
|
34
|
+
const ctx = new Context();
|
|
35
|
+
const loader = new Loader(ctx, { moduleHost: new ModuleHost({ rootDirectory: launch.directory }), allowPending: launch.allowPending });
|
|
36
|
+
host = {
|
|
37
|
+
diagnostics: () => JSON.parse(JSON.stringify(loader.diagnostics())),
|
|
38
|
+
call: async (service, member, args) => {
|
|
39
|
+
const value = ctx.get(service);
|
|
40
|
+
if (value === undefined || typeof value[member] !== 'function') throw new LoaderError('SERVICE_METHOD', `Missing service method ${service}.${member}`);
|
|
41
|
+
return Reflect.apply(value[member], value, args);
|
|
42
|
+
},
|
|
43
|
+
close: async () => { await loader.dispose(); await ctx.dispose(); },
|
|
44
|
+
};
|
|
45
|
+
await loader.loadFile(join(launch.directory, launch.entry));
|
|
46
|
+
}
|
|
47
|
+
await channel.send({ type: 'ready', diagnostics: jsonValue(await host.diagnostics()) });
|
|
48
|
+
})().catch(async error => {
|
|
49
|
+
startupError = error;
|
|
50
|
+
await channel.send({ type: 'startup-error', error: serialize(error) });
|
|
51
|
+
});
|
|
52
|
+
startup.catch(() => {});
|
|
53
|
+
channel.listen(request => {
|
|
54
|
+
if (!request || typeof request !== 'object' || Array.isArray(request) || !Number.isSafeInteger(request.id) || typeof request.method !== 'string') return;
|
|
55
|
+
const execute = async () => {
|
|
56
|
+
if (request.method === 'shutdown') {
|
|
57
|
+
closing = true;
|
|
58
|
+
shutdown ??= (async () => {
|
|
59
|
+
await startup;
|
|
60
|
+
await Promise.allSettled([...calls]);
|
|
61
|
+
if (!host) throw new LoaderError('CLEANUP_BLOCKED', 'Startup did not expose a host cleanup controller; explicit abandonment is required', {}, startupError);
|
|
62
|
+
await host.close();
|
|
63
|
+
return { disposed: true };
|
|
64
|
+
})();
|
|
65
|
+
try { return await shutdown; } catch (error) { shutdown = undefined; throw error; }
|
|
66
|
+
}
|
|
67
|
+
if (closing) throw new LoaderError('DOMAIN_CLOSING', 'The isolated domain is closing');
|
|
68
|
+
await startup;
|
|
69
|
+
if (startupError) throw startupError;
|
|
70
|
+
if (request.method === 'diagnostics') return jsonValue(await host.diagnostics());
|
|
71
|
+
if (request.method === 'moduleGraph') return graph.snapshot();
|
|
72
|
+
if (request.method !== 'call') throw new LoaderError('DOMAIN_METHOD', 'Unsupported isolated domain request');
|
|
73
|
+
const { service, member, args } = request;
|
|
74
|
+
if (['constructor', '__proto__', 'prototype'].includes(member)) throw new LoaderError('SERVICE_METHOD', 'Reserved service method');
|
|
75
|
+
return jsonValue(await host.call(service, member, args));
|
|
76
|
+
};
|
|
77
|
+
const task = execute();
|
|
78
|
+
if (request.method !== 'shutdown') calls.add(task);
|
|
79
|
+
task.then(async result => {
|
|
80
|
+
await channel.send({ type: 'reply', id: request.id, result });
|
|
81
|
+
if (request.method === 'shutdown') channel.close();
|
|
82
|
+
}, error => channel.send({ type: 'reply', id: request.id, error: serialize(error) }))
|
|
83
|
+
.catch(() => {}).finally(() => calls.delete(task));
|
|
84
|
+
});
|
|
85
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Context, Fiber, Plugin } from '@validation-engineering/cordis';
|
|
2
|
+
export type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
|
|
3
|
+
export type Injection = string[] | Record<string, JSONValue>;
|
|
4
|
+
export type Isolation = string[] | Record<string, true | string>;
|
|
5
|
+
export interface EntryOptions {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
config?: JSONValue;
|
|
9
|
+
inject?: Injection;
|
|
10
|
+
isolate?: Isolation;
|
|
11
|
+
intercept?: Record<string, JSONValue>;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
group?: boolean;
|
|
14
|
+
entries?: EntryOptions[];
|
|
15
|
+
include?: string;
|
|
16
|
+
}
|
|
17
|
+
export type ConfigTree = EntryOptions[] | { entries: EntryOptions[] };
|
|
18
|
+
export type EntryState = 'pending' | 'loading' | 'active' | 'failed' | 'disposed' | 'unloading' | 'disabled';
|
|
19
|
+
export interface EntryDiagnostic {
|
|
20
|
+
id: string;
|
|
21
|
+
parentId: string | null;
|
|
22
|
+
moduleURL: string | null;
|
|
23
|
+
state: EntryState;
|
|
24
|
+
unavailableServices: string[];
|
|
25
|
+
fiberId?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Already prepared executable factory; this does not invalidate Node module caches. */
|
|
28
|
+
export interface PreparedModule { plugin: Plugin; namespace?: unknown; revision?: string | number; }
|
|
29
|
+
export interface LoadedModule extends PreparedModule { url: string; directory: string; }
|
|
30
|
+
export interface EntryNode {
|
|
31
|
+
id: string;
|
|
32
|
+
options: EntryOptions;
|
|
33
|
+
baseURL: string;
|
|
34
|
+
group: boolean;
|
|
35
|
+
children: EntryNode[];
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
module?: LoadedModule;
|
|
38
|
+
}
|
|
39
|
+
export interface Recipe { tree: EntryNode[]; files: string[]; }
|
|
40
|
+
export class LoaderError extends Error {
|
|
41
|
+
readonly code: string;
|
|
42
|
+
readonly details: Record<string, unknown>;
|
|
43
|
+
constructor(code: string, message: string, details?: Record<string, unknown>, cause?: unknown);
|
|
44
|
+
}
|
|
45
|
+
export class ModuleHost {
|
|
46
|
+
constructor(options?: { rootDirectory?: string; loadModule?: (canonicalURL: string) => PreparedModule | Promise<PreparedModule> });
|
|
47
|
+
resolve(specifier: string, baseURL: string): Promise<string>;
|
|
48
|
+
load(specifier: string, baseURL: string): Promise<LoadedModule>;
|
|
49
|
+
prepare(input: Recipe): Promise<Recipe>;
|
|
50
|
+
reset(): never;
|
|
51
|
+
}
|
|
52
|
+
export function readConfig(input: string | URL | ConfigTree, options?: { baseURL?: string; rootDirectory?: string }): Promise<Recipe>;
|
|
53
|
+
export class Include {
|
|
54
|
+
static read(filename: string | URL, options?: { baseURL?: string; rootDirectory?: string }): Promise<Recipe>;
|
|
55
|
+
}
|
|
56
|
+
export class Entry {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly parentId?: string;
|
|
59
|
+
readonly options: Readonly<EntryOptions>;
|
|
60
|
+
readonly disabled: boolean;
|
|
61
|
+
readonly moduleURL?: string;
|
|
62
|
+
readonly fiber?: Fiber;
|
|
63
|
+
readonly context?: Context;
|
|
64
|
+
readonly state: EntryState;
|
|
65
|
+
}
|
|
66
|
+
export class Loader {
|
|
67
|
+
constructor(context?: Context, options?: { moduleHost?: ModuleHost; baseURL?: string; allowPending?: boolean });
|
|
68
|
+
readonly ctx: Context;
|
|
69
|
+
readonly moduleHost: ModuleHost;
|
|
70
|
+
readonly state: 'empty' | 'reloading' | 'active' | 'blocked' | 'closed';
|
|
71
|
+
readonly revision: number;
|
|
72
|
+
readonly lastRecovery?: { restored: true; cause: unknown };
|
|
73
|
+
entries(): IterableIterator<Entry>;
|
|
74
|
+
resolve(id: string): Entry;
|
|
75
|
+
apply(tree: ConfigTree, options?: { baseURL?: string }): Promise<EntryDiagnostic[]>;
|
|
76
|
+
loadFile(filename: string | URL): Promise<EntryDiagnostic[]>;
|
|
77
|
+
reload(): Promise<EntryDiagnostic[]>;
|
|
78
|
+
update(id: string, patch: Partial<Pick<EntryOptions, 'name' | 'config' | 'inject' | 'isolate' | 'intercept' | 'disabled'>>): Promise<EntryDiagnostic[]>;
|
|
79
|
+
setEnabled(id: string, enabled: boolean): Promise<EntryDiagnostic[]>;
|
|
80
|
+
diagnostics(): EntryDiagnostic[];
|
|
81
|
+
retryCleanup(): Promise<void>;
|
|
82
|
+
dispose(): Promise<void>;
|
|
83
|
+
}
|