mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
docs(chord): slice 7c spec (plugins), per plugin at startup and reload
This commit is contained in:
parent
50af8c1601
commit
18a155e2f5
5 changed files with 230 additions and 24 deletions
|
|
@ -1838,6 +1838,35 @@ validation and copies the v2 kernel added around Delta.
|
|||
key hold two pins (counted on the key's one source; the document is loaded and kept once). **Binary append commit-to-frame
|
||||
median +10% accepted** (owner, 2026-10-29; CPU 10, 0.031 → 0.034-0.035 ms): equal with the young generation pinned, fewer
|
||||
scavenges over the whole benchmark (V8's young-generation sizing, slice 7b fix round 3).
|
||||
- **Per plugin at startup and reload (owner, 2026-10-30: "yes", "1. lgtm 2. yes"; analyses
|
||||
`/tmp/per-plugin-{opus,fable}.md`):** supersedes "Rollback is all-or-nothing per host" (2026-10-2x, 7b §2, design §21)
|
||||
and 7c's Q2 ("a facet throwing while evaluated fails its host's whole step"). Why all or nothing was chosen: per plugin
|
||||
was decided first and p2's plugin reviewers found counter-traces (a dropped argument field written into a transcript; a
|
||||
copied Durable document written by two versions; a provider's failed factory leaving the harness hanging; mixed copied
|
||||
code and registries); v2 has since closed them (closed argument schemas for every caller, Q12, Q15; version binding
|
||||
with waiting, 7a; ownership of documents; registry versions with `accepts`, Q8; copied cross-plugin files limited to
|
||||
contracts without dependencies, 7c). Rules: (1) **at startup** a plugin whose build, evaluation or setup fails is left
|
||||
out; `load` resolves with a report `{ failed: [{ plugin, error }] }` and never closes the host for it (the core facet
|
||||
included: base code decides whether that is fatal); its consumers wait `failed { waiting }` as for any missing plugin.
|
||||
(2) **At reload** a plugin whose new code fails (build, evaluation, setup, a factory in staging, a declaration error,
|
||||
`checkAccepts`) keeps its old code (its loaded facet, unchanged), and every other plugin of the reload applies; the
|
||||
report lists it (`kept: [{ plugin, error }]`); staging retries without it. (3) **The closure rule** (Opus): after
|
||||
staging, a changed plugin whose recorded requests would not bind because the plugin they need was kept old or left
|
||||
out is kept old too, so a consumer edited together with a broken provider keeps working on its old code. (4) What no
|
||||
rule detects: a consumer relying on new behaviour of a kept provider whose contract (version, fingerprint) did not
|
||||
change: the same risk as a rollout across hosts, documented. (5) `rollout` keeps a plugin dirty while it was kept old
|
||||
on any host. Built with slice 7c (the kernel's `load` and `reload` change with it).
|
||||
- **Slice 7c's points (owner, 2026-10-30; `slice-7c.md` §5):** (1) esbuild is an optional peer dependency of Chord,
|
||||
imported only by the plugins component. (2) **No dev/prod split** (owner: "chord apps are supposed to not have a
|
||||
prod/dev split. they are supposed to be self modifying applications"): watching, rebuilding and rolling out are core
|
||||
machinery; one component `chord/next/plugins` (discovery, build, evaluation, `pluginFile`, the watcher, `rollout`) in
|
||||
`src/next-node/plugins/`, exported with v2's entry point; it turns V8's compilation cache off when created; reloads
|
||||
stay explicit (the watcher marks, never applies). Supersedes the separate `loader` and `dev` subpaths of "Slice 7b's
|
||||
open points" (5), (6). (3) `installDependencies(plugin, ctx)` as coding-agent installs package dependencies (`npm
|
||||
install --omit=dev --legacy-peer-deps --ignore-scripts --no-audit --no-fund` in the plugin's real directory, the
|
||||
command overridable). (4) Cross-plugin imports: any file of a discovered plugin, copied, documented as contracts and
|
||||
side-effect-free modules only, listed by the build, no static check yet. (5) `pluginFile` answered per facet by the
|
||||
`require` evaluation passes.
|
||||
- **A plugin's npm dependencies are bundled; external only by opt-in (owner, 2026-10-29: "explicit opt in + (a)"):**
|
||||
a plugin's `dependencies` are installed into its own `node_modules` (`npm install --ignore-scripts`) and bundled into
|
||||
each facet that imports them (host packages, its `peerDependencies`, stay external and are passed in). On Node a
|
||||
|
|
@ -2395,7 +2424,7 @@ From `research/chord-on-durable-objects.md`, adopted for the Chord v2 kernel bec
|
|||
|
||||
- **Per host:** load (bundle; a load error rejects) → staging (run the facet setup and every live object's new factory
|
||||
with a recording `self`, declarations only, nothing activated; a throw rejects) → commit (atomic) → after.
|
||||
- **Rollback is all-or-nothing per host:** any load or staging error leaves the host entirely on its previous code.
|
||||
- ~~**Rollback is all-or-nothing per host:**~~ (**superseded** by "Per plugin at startup and reload", 2026-10-30) any load or staging error leaves the host entirely on its previous code.
|
||||
A host is completely at revision R or R−1, so status per host suffices.
|
||||
- **Pure objects** (**superseded in part** by R15 (old code touching a document migrated past its version throws)) (no `self.open`, old or new) are swapped in place: new methods and derives; documents, references
|
||||
and jobs stay; routines restart serialized; new references become live; then the object's gate opens. Running calls
|
||||
|
|
|
|||
|
|
@ -231,8 +231,8 @@ parent link closes. `resolve` returns the link before the worker has loaded (the
|
|||
| Call | Meaning |
|
||||
| --- | --- |
|
||||
| `createHost<P>({ address?, principal?, ...options })` | with an address: addressable, serves only objects placed there, acts as `principal` on its own host; without: a client. `P` is the app's principal type |
|
||||
| `load([{ name, facet }], context)` | facets with their plugin names (the plugin's `package.json` name, npm style; duplicates and invalid names are refused); runs every facet's setup, publishes all of them in one batch (types and registries served, registries composed once, contributions; requests waiting for the publication are placed after it), then waits for the start barrier (section 21: the first outcomes of its host-lifetime objects, slice 7a); a throwing setup rejects and closes the host; runs once. Loaders are adapters; base code passes its core facet the same way |
|
||||
| `reload([{ name, facet }], context)` | the complete new plugin set after `load` (a facet passed as the same object is an unchanged plugin): staging, then one commit, then every object of a changed or removed plugin takes its fate; resolves with a report, rejects with a staging error and nothing changed; one at a time (section 21, slice 7b) |
|
||||
| `load([{ name, facet }], context)` | facets with their plugin names (the plugin's `package.json` name, npm style; duplicates and invalid names are refused); runs every facet's setup, publishes all of them in one batch (types and registries served, registries composed once, contributions; requests waiting for the publication are placed after it), then waits for the start barrier (section 21: the first outcomes of its host-lifetime objects, slice 7a); a plugin whose setup throws is left out and reported (`load` resolves with `{ failed: [{ plugin, error }] }`, decision "Per plugin at startup and reload"); runs once. Loaders are adapters; base code passes its core facet the same way |
|
||||
| `reload([{ name, facet }], context)` | the complete new plugin set after `load` (a facet passed as the same object is an unchanged plugin): staging, then one commit, then every object of a changed or removed plugin takes its fate; resolves with a report (a plugin whose new code fails keeps its old code, listed as `kept`); one at a time (section 21, slice 7b) |
|
||||
| `connect(link, { principal, trusted?, codec? }, context)` | the link to the parent, at most one open; once it closed (a drop), `connect` takes the next, and every remote reference over the parent is held again on it. `principal` = who the parent is |
|
||||
| `accept(link, { principal, trusted?, codec? }, context)` | a link from a child; holds and calls on it act as `principal` |
|
||||
| `route(Kind, { authorize, resolve }, context)` | how to reach hosts of a kind (below); called before `load`, once per kind; work arriving on its links runs under `context` |
|
||||
|
|
@ -1810,7 +1810,8 @@ agent asks everyone present. Its store must live in an object (it predates "nati
|
|||
changed (Chord compares no code); one passed as the very same facet object is unchanged and untouched (owner,
|
||||
2026-10-29: base code passes its core facet value every time); one loaded and not in it is removed. It resolves with a
|
||||
report once the commit happened and every object took its fate (swaps and retries had their first dependency check;
|
||||
it does not wait for objects to go live), and rejects with the code error when staging fails, nothing changed.
|
||||
it does not wait for objects to go live); a plugin whose new code fails keeps its old code and is listed in the
|
||||
report's `kept` (per plugin, below).
|
||||
Reloads run one after another per host; before `load` resolved it throws, after `close` it rejects `released/closed`;
|
||||
`context` bounds it (decision 4: no install watchdog): its abort before the commit rejects `aborted` and changes
|
||||
nothing, after the commit it only ends the wait.
|
||||
|
|
@ -1818,6 +1819,7 @@ nothing, after the commit it only ends the wait.
|
|||
```ts
|
||||
type ReloadReport = {
|
||||
readonly plugins: { readonly added: readonly string[]; readonly changed: readonly string[]; readonly removed: readonly string[] };
|
||||
readonly kept: readonly { readonly plugin: string; readonly error: unknown }[]; // new code failed: old code kept (per plugin)
|
||||
readonly objects: readonly ({ readonly object: string } & (
|
||||
| { readonly kind: "swapped" } | { readonly kind: "restarted" } | { readonly kind: "ended" }
|
||||
| { readonly kind: "parked"; readonly cause: FailureCause } | { readonly kind: "retried" }))[];
|
||||
|
|
@ -1832,11 +1834,18 @@ type ReloadReport = {
|
|||
4. after each object takes its fate (below)
|
||||
```
|
||||
|
||||
- **Rollback is all-or-nothing per host:** any load or staging error (a setup's or factory's throw, a declaration
|
||||
error, `checkAccepts`, a type or registry served twice, a remote type on a host without principal or address)
|
||||
rejects the reload naming the plugin (and object), and the host stays entirely on its old code; a host is at
|
||||
revision R or R−1. Runtime states (registry conflicts, failed opens, migrations) are not rollback reasons: they are
|
||||
per object after the commit (`onError` as always; no new `HostError` kind, the report is the summary).
|
||||
- **Per plugin, never all or nothing** (owner, 2026-10-30, decision "Per plugin at startup and reload"; supersedes 7b's
|
||||
rollback per host): a plugin whose new code fails at a reload (its build or evaluation, a setup's or factory's throw
|
||||
in staging, a declaration error, `checkAccepts`, a type or registry served twice, a remote type on a host without
|
||||
principal or address) keeps its old code (its loaded facet, unchanged) and is reported in the reload's report
|
||||
(`kept: [{ plugin, error }]`); staging runs again without it, and every other plugin of the reload applies. **The
|
||||
closure rule:** a changed plugin whose recorded requests would not bind because the plugin they need was kept old (or
|
||||
is missing) is kept old too, so a consumer edited together with a broken provider keeps working on its old code. At
|
||||
startup the same holds: `load` leaves a failing plugin out and resolves with `{ failed: [{ plugin, error }] }`; its
|
||||
consumers wait `failed { waiting }`; `load` never closes the host for a plugin (base code decides whether its core
|
||||
failing is fatal). What no rule detects: a consumer relying on new behaviour of a kept provider whose contract did not
|
||||
change (as in a rollout across hosts). Runtime states (registry conflicts, failed opens, migrations) stay per object
|
||||
after the commit (`onError` as always).
|
||||
- **Staging** runs every facet's setup into a staged table beside the live one (types with their factory, `linger` and
|
||||
provider, `bindProvider(contract, options.accepts)`; registry providers; facet contributions by plugin), then, for
|
||||
every live, activating, parked or failed object of a changed or removed plugin whose type is still served, its new
|
||||
|
|
@ -1922,18 +1931,30 @@ type ReloadReport = {
|
|||
unchanged server restart reinstalls nothing), serialized installs, a host that starts or reconnects installing the
|
||||
current set before anything is routed to it, a rejected own install failing its own requests with the reason.
|
||||
|
||||
### Loading and the dev loop
|
||||
### Loading plugins and the reload loop (slice 7c)
|
||||
|
||||
- Plugins come from built-in, global (`<data>/plugins`), package (npm/git, installed with `--ignore-scripts`) and
|
||||
project roots, found by directory listing (no source parsing); the server selects; dependencies resolve by contract
|
||||
name at runtime; names are prefixed (`<plugin>.<name>`; tools `<plugin>_<name>`).
|
||||
- **Loader D:** one base bundle per surface (host packages and the app runtime) plus one script per facet (host
|
||||
packages passed in, no module map); old generations are reclaimed by the GC; stable filename per (facet, digest) and
|
||||
V8's compilation cache off on hosts that reload in place. The loader is an adapter: static imports (tests, Durable
|
||||
Objects) work the same.
|
||||
- Project plugins, trust, snapshots and scopes: `apps/p2/docs/plugin-loading.md` (still the reference for those).
|
||||
- **Dev loop:** a source watcher marks plugins dirty (never applies changes); a reload control (playground web shell,
|
||||
CLI, agent tool) reloads the changed plugins or restarts all hosts for base code.
|
||||
- **No dev/prod split** (owner, 2026-10-30): a Chord app is self-modifying (the owner or an agent edits a plugin while
|
||||
it runs), so finding, building, loading, watching and rolling out plugins is core machinery: one component,
|
||||
`chord/next/plugins` (`src/next-node/plugins/`, Node-side; the kernel stays portable and takes facets however they
|
||||
were made: static imports in tests and Durable Objects work the same). Reloads stay explicit: the watcher marks,
|
||||
never applies.
|
||||
- **A plugin** is a directory with a `package.json` (its `name` is the plugin's name, npm style) and one file per facet
|
||||
kind the app names; plugins come from roots found by directory listing (no source parsing; the first root wins a
|
||||
duplicate name). Dependencies between plugins resolve by contract name at runtime; names are prefixed
|
||||
(`<plugin>.<name>`; tools `<plugin>_<name>`).
|
||||
- **Loader D:** one esbuild script per facet (its own files, copied files of other plugins, its npm `dependencies`
|
||||
inlined; host packages, `chord.external` packages and Node builtins external, passed in at evaluation); evaluated with
|
||||
`vm.Script` under a stable filename per (plugin, kind, digest); no module map: old generations are reclaimed by the
|
||||
GC; V8's compilation cache turned off when the component is created. `pluginFile(path)` resolves a file of the
|
||||
calling facet's own plugin. A plugin's dependencies are installed into its directory (`installDependencies`, as
|
||||
coding-agent does: `--omit=dev --legacy-peer-deps --ignore-scripts`). esbuild is an optional peer dependency.
|
||||
- **The reload loop:** a watcher marks plugins dirty from the files each facet used (a copied contract dirties every
|
||||
user) and flags changes of base code (the app restarts its hosts); a build is all or nothing; `rollout` runs the app's
|
||||
steps in order (server, workers, clients; decision "Slice 7b's open points" (3)). A reload control (a button, a CLI,
|
||||
an agent tool) is the app's. One base bundle per surface (host packages and the app runtime) is an app's startup
|
||||
optimization, the web's in slice 11.
|
||||
- Project plugins, trust, snapshots, scopes and npm/git package installs: `apps/p2/docs/plugin-loading.md` (still the
|
||||
reference for those; not in 7c).
|
||||
|
||||
- **UI reload vs. leaving:** "leaving" is never derived from an unmount, only from explicit navigation or presence (a
|
||||
dropped reference); a component unmounted by a reload has not left.
|
||||
|
|
@ -2409,8 +2430,7 @@ with their contributor, readable contributions and readers looking up the curren
|
|||
registry keys with a `conflict` flag;
|
||||
`restart_required`; detached references counting toward presence; contracts taken from the running app; ceremony
|
||||
(`native/` directories, `defineNative`, lints); weakening type checking; "magic" input-field matching for delete; a
|
||||
factory trial; a migrate dry run; principal field patterns and re-stamping for revocation; `storeKey`; a consumer contract check during staging; reload votes; per-plugin rollback within a
|
||||
host; `defaultProjectTrust: "always"`; loader variants A and B; a topology layer above the host API; a stream type in
|
||||
factory trial; a migrate dry run; principal field patterns and re-stamping for revocation; `storeKey`; a consumer contract check during staging; reload votes; `defaultProjectTrust: "always"`; loader variants A and B; a topology layer above the host API; a stream type in
|
||||
Chord; callbacks as method arguments; `opaque` members; numeric middleware priorities; host addresses as data naming an
|
||||
input field; host-name strings in app code; host values (start data and platform bindings are core objects);
|
||||
consumer-side linger; registry composition as a staging check; a JSONL document storage; `tx.get` for documents; validating state and documents on commits or reads (migration results are validated once, R8); copying values on same-host calls; op-rewriting projections and a
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ type ReloadedObject = { readonly object: string } & (
|
|||
(decision 4: no install watchdog); its abort before the commit rejects `aborted` and changes nothing; after the commit
|
||||
it only ends the wait.
|
||||
|
||||
## 2. Staging (all or nothing)
|
||||
## 2. Staging (all or nothing; **superseded** 2026-10-30 by "Per plugin at startup and reload": a failing plugin keeps its old code, slice 7c §4)
|
||||
|
||||
1. **Setups.** Every facet's setup runs into a **staged** table, beside the live one: served types (with their factory,
|
||||
options and `bindProvider(contract, options.accepts)`, which runs `checkAccepts`), served registries (provider
|
||||
|
|
|
|||
157
packages/chord/docs/next/slice-7c.md
Normal file
157
packages/chord/docs/next/slice-7c.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Slice 7c: plugins (spec)
|
||||
|
||||
Status: **ready to build** (owner, 2026-10-30: decisions "Slice 7c's points" and "Per plugin at startup and reload";
|
||||
spec reviews by Fable and Opus, `/tmp/s7c-spec-review-{fable,opus}.md`, folded in). 7c builds `chord/next/plugins`: the
|
||||
code that turns plugin directories into facets, loads and reloads them per plugin, and watches them. A Chord app is
|
||||
self-modifying (the owner or an agent edits a plugin while it runs), so this is core machinery, not a dev tool (no
|
||||
dev/prod split). It also changes the kernel's `load` and `reload` to per-plugin failure (§4). HTTP and workers are slice
|
||||
10, the web page host and the TUI slice 11: 7c builds scripts for every surface and evaluates them on Node; its build
|
||||
results are plain data those slices ship.
|
||||
|
||||
Read in full before writing code: this spec; `decisions.md` (2026-10-29/30: "Slice 7b's open points, as recommended",
|
||||
"A plugin's npm dependencies are bundled", "Per plugin at startup and reload", "Slice 7c's points"); `design.md` (§3
|
||||
`load`/`reload`, §16, §18, §21 in full); the 7b brief `slice-7b/book.md` (chapters "A Plugin, File by File", "Building
|
||||
and Loading", "Assets and Files", "Performance", "A Mini p2 from Chord"); the two spec reviews; p2's
|
||||
`apps/p2/src/hosts/{bundles,plugins}.ts` and `apps/p2/docs/plugin-loading.md` §4.1, §7.1, §8, §9, §14; coding-agent's
|
||||
`packages/coding-agent/src/core/package-manager.ts` (dependency installs). Code lives in
|
||||
`packages/chord/src/next-node/plugins/` (Node-only: `fs`, esbuild, `vm`; the kernel in `src/next` stays portable),
|
||||
exported as `./next/plugins` when v2 gets its entry point; until then tests import the source.
|
||||
|
||||
## 1. A plugin on disk
|
||||
|
||||
```text
|
||||
plugins/notes/
|
||||
package.json "name": the plugin's name (required, npm style); "dependencies": bundled; "peerDependencies":
|
||||
host packages; "chord": { "external": ["better-sqlite3"] } (optional, Node facets only)
|
||||
server.ts worker.ts web.tsx tui.ts one file per facet kind the app names; default export: a facet
|
||||
contract.ts store.ts NoteList.tsx notes.css assets/… plain files
|
||||
node_modules/ its own npm dependencies (`installDependencies`)
|
||||
```
|
||||
|
||||
- The plugin's name is its `package.json` `name`, never the directory name. A directory without a `package.json`, or
|
||||
with an invalid name, is reported, not a plugin.
|
||||
- Facet kinds are the app's (`["server", "worker", "web", "tui"]` in p3), each `.ts`, `.tsx`, `.js` or `.mjs`; two
|
||||
files of one kind are reported (not chosen by extension). A plugin with none of the kinds (contracts only) is a
|
||||
plugin with no facets: imported by others, never loaded.
|
||||
- Discovery lists each root one level deep, reads only `package.json`, follows symlinks to their real paths, and keeps
|
||||
the first root's plugin of a duplicate name (the other reported).
|
||||
|
||||
## 2. Building
|
||||
|
||||
```ts
|
||||
const plugins = await discoverPlugins(["./plugins", join(dataDir, "plugins")], { kinds: KINDS }, ctx);
|
||||
const built = await buildFacet(plugins[0], "worker", { hostPackages: HOST_NAMES, platform: "node" }, ctx);
|
||||
// { kind: "script", plugin, facet: "worker", source, digest, inputs: [{ file, hash }], copied: [file],
|
||||
// css?: { name, text }, assets: [{ name, bytes }] } | { kind: "failed", plugin, facet, errors }
|
||||
```
|
||||
|
||||
One esbuild build per (plugin, kind), from cold (no kept contexts: about 10 ms per facet; p2 measured 237 ms for 25
|
||||
worker facets). A failed build is a result, never a throw. The result is plain data (strings, bytes, arrays): the same
|
||||
value a worker (slice 10) or a page (slice 11) evaluates where it runs.
|
||||
|
||||
| An import of | Becomes |
|
||||
| --- | --- |
|
||||
| the plugin's own files | inlined |
|
||||
| a file of another discovered plugin (`../sessions/contract.ts`) | inlined (a copy), listed in `copied`; it may import only host packages and files of its own plugin, never packages (a build error otherwise): contracts and side-effect-free modules only (decision 4) |
|
||||
| a package in the plugin's `dependencies` | inlined from its `node_modules` |
|
||||
| a package in `chord.external` (Node builds) | external, `require`d at evaluation from the plugin's `node_modules`; a browser build fails |
|
||||
| a host package (`options.hostPackages`, e.g. `@earendil-works/chord/next`, `react`, `react/jsx-runtime`, `@app/core`) | external, from the map at evaluation |
|
||||
| `@earendil-works/chord/next/plugins` | external always (its `pluginFile` is per facet, §3) |
|
||||
| a Node builtin (`node:fs`, `fs`) | external (Node builds); a browser build fails |
|
||||
| CSS (browser builds) | one stylesheet per facet (`css`) |
|
||||
| an asset (`import url from "./icon.svg?url"`, `.wasm`, browser builds) | a content-hashed file in `assets`; the import is its URL under the app's `options.publicPath` |
|
||||
| `import.meta.url`, `__dirname`, `__filename`, dynamic `import()`, top-level `await` | a build error pointing to `pluginFile` (§3) |
|
||||
| anything else (a relative path into base code, a package not in `dependencies`) | a build error naming the file and the rule |
|
||||
|
||||
- Output: `format: "cjs"`, wrapped `(function (require, module, exports) { … })`; TypeScript stripped, JSX automatic
|
||||
(`react/jsx-runtime`); no type check; minified; an inline source map whose `sources` are relative to the plugin's
|
||||
directory (so the script and its `digest`, a hash of the source, are the same on every machine and checkout).
|
||||
- `inputs` are captured where esbuild loads each file (an `onLoad` that hashes the exact bytes it hands esbuild, as p2's
|
||||
`captureBundleInputs`; the metafile has no hashes), absolute paths, for the watcher.
|
||||
|
||||
## 3. Evaluating (Node)
|
||||
|
||||
```ts
|
||||
const facet = evaluateFacet(built, { hostPackages: HOST, plugin }); // returns the facet, or throws its error
|
||||
```
|
||||
|
||||
- `vm.Script` under a stable filename per (plugin, kind, digest), path-like (`/chord-plugins/@pi/notes/worker.ts`
|
||||
plus the digest), run in this context; the default export, checked to be a facet (a declaration error otherwise).
|
||||
- **The `require` it passes answers, in order:** the host-package map (each value is what Node's `require` would give:
|
||||
a CommonJS package's exports, an ESM package's namespace wrapped once with `__esModule: true`, so default and named
|
||||
imports both work); `@earendil-works/chord/next/plugins` → `{ pluginFile }` bound to this plugin's directory;
|
||||
`chord.external` packages and Node builtins through `createRequire(join(plugin.dir, "package.json"))`; anything else
|
||||
throws naming the facet.
|
||||
- **`pluginFile(path)`** gives the absolute path of a file inside the plugin's directory; `..` out of it is refused.
|
||||
- **No module map, no cache:** an old facet's closures are collected once nothing holds them. The component turns V8's
|
||||
compilation cache off when it is created (`v8.setFlagsFromString("--no-compilation-cache")`, once per process; an
|
||||
app may pass the flag itself).
|
||||
- **Stack traces** (decision Q1 (b)): `mapStack(error)` returns the error's stack with plugin scripts' positions mapped
|
||||
through their source maps (a `module.SourceMap` per evaluated script, kept while the script is) to `notes/worker.ts:12`;
|
||||
the app's reporters call it. Nothing global (no `Error.prepareStackTrace`, no process-wide source-map switch).
|
||||
|
||||
## 4. Loading and reloading, per plugin (kernel change)
|
||||
|
||||
Decision "Per plugin at startup and reload" changes `host.load` and `host.reload` (slice 7b) and the component's
|
||||
`load`/`reload` helpers:
|
||||
|
||||
- **`host.load(facets, ctx)`** resolves with `{ failed: [{ plugin, error }] }`: a plugin whose setup throws (or that is
|
||||
refused at declaration: invalid name, a type or registry served twice, a remote type on a host without principal or
|
||||
address, `checkAccepts`) is left out and reported; the others publish; its consumers wait `failed { waiting }`. `load`
|
||||
never closes the host for a plugin (base code decides whether its core failing is fatal).
|
||||
- **`host.reload(facets, ctx)`**: staging per plugin. A plugin whose new code fails in staging (setup, a factory against
|
||||
the recording `self`, a declaration error, `checkAccepts`) keeps its loaded facet (as if passed unchanged), and
|
||||
staging runs again without its new code. **The closure rule:** a changed plugin whose recorded requests (staging
|
||||
records them with their contract versions) would not bind because the plugin they need was kept old, or is missing,
|
||||
is kept old too (repeat until stable). The report gains `kept: [{ plugin, error }]`; the commit applies the rest.
|
||||
- **The component** builds and evaluates per plugin: a plugin whose build fails or whose evaluation throws is passed to
|
||||
the host as its loaded facet (kept) at a reload, left out at startup, and reported with the host's report.
|
||||
`plugins.load(host, ctx)` and `plugins.reload(host, names, ctx)` keep, per host, the facet loaded for every plugin, so
|
||||
`reload` always passes the complete set (unchanged plugins as the same facet object, removed ones left out) and never
|
||||
restarts an unchanged plugin.
|
||||
|
||||
## 5. Watching and rollout
|
||||
|
||||
```ts
|
||||
const plugins = createPlugins({ roots: ["./plugins"], kinds: KINDS, hostPackages: HOST, base: ["./src"] }, ctx);
|
||||
plugins.state // a JSON watchable: { dirty: ["@pi/notes"], install: ["@pi/notes"], set: false, base: false }
|
||||
const next = await plugins.build(["@pi/notes"], ctx); // built results per (plugin, kind), plain data
|
||||
await rollout([serverStep, ...workerSteps, ...clientSteps], next, ctx); // the app's order (decision 3)
|
||||
```
|
||||
|
||||
- **The watcher** (`fs.watch` recursive over roots and `base`) ignores `node_modules` and `.git`. A changed file marks
|
||||
dirty every plugin whose facets used it (`inputs`; a copied contract dirties every user) or whose directory contains
|
||||
it; a changed `package.json` marks `install`; a plugin directory added or removed sets `set` (discovery runs again
|
||||
at the next build); a file under `base` sets `base` (the app restarts its hosts; Chord restarts nothing). A change
|
||||
during a build keeps its mark. If the watcher cannot watch (the system's limit), it reports once and the app builds
|
||||
on demand. It marks, never applies.
|
||||
- **`installDependencies(plugin, ctx)`**: `npm install --omit=dev --legacy-peer-deps --ignore-scripts --no-audit
|
||||
--no-fund` in the plugin's real directory, the command overridable (decision 3); it clears `install`.
|
||||
- **`rollout(steps, built, ctx)`**: each step is `{ name, kinds, reload(built, ctx) }` and returns the host's report;
|
||||
steps run in order; a step's `reload` evaluates the facets of its kinds where its host runs (an in-process step through
|
||||
the component's per-host state, §4; a remote one, slice 10/11, gets the built data). It stops at a step that rejects
|
||||
(a host closed, an abort); kept plugins do not stop it. Marks clear for plugins applied on every step; a plugin kept
|
||||
on any host stays dirty.
|
||||
|
||||
## 6. Not in 7c
|
||||
|
||||
Installing plugins from npm or git (p2 §8), project plugins with trust and snapshots (design §26 #14), a base bundle per
|
||||
surface, evaluating in a browser (slice 11), shipping built scripts to workers and pages (slices 10, 11), static checks
|
||||
of cross-plugin imports beyond §2's rule.
|
||||
|
||||
## 7. Tests, examples, cost
|
||||
|
||||
- One test file per source file; fixture plugins under `test/next-node/fixtures/plugins/`. Discovery (names, duplicates,
|
||||
missing or invalid `package.json`, two files of a kind, contracts-only plugins, symlinks); build (every row of §2's
|
||||
table, failures as results, relative source maps and stable digests across two checkout paths, inputs hashed at
|
||||
load); evaluation (host packages as the host's instances: `instanceof ChordError` across a plugin; default and named
|
||||
imports of ESM and CommonJS host packages; `pluginFile` and its `..` refusal; an import-time throw; a non-facet
|
||||
export); `mapStack` naming plugin files and lines; per plugin at startup and reload (§4: a failing setup, factory,
|
||||
declaration, build, evaluation; the closure rule with a provider and consumer changed together; the report's `failed`
|
||||
and `kept`; `load` never closing the host); the watcher (dirty marks from inputs and directories, `install`, `set`,
|
||||
`base`, a change during a build, `node_modules` ignored); `rollout` (order, stopping at a rejecting step, marks).
|
||||
- **GC:** 200 reloads of an evaluated plugin facet: old closures collected, heap flat after warm-up.
|
||||
- **End to end:** a server and a worker (memory links) loading plugin directories, a plugin edited on disk, built and
|
||||
rolled out (server, then worker), a holder seeing one frame; a broken edit kept old on both hosts and reported.
|
||||
- Example `35-plugins.ts`. Benchmark (`benchmarks.md`): cold build of a 20-plugin set, a rebuild of one facet,
|
||||
evaluation per facet, heap after 100 reloads.
|
||||
|
|
@ -56,7 +56,7 @@ telemetry last, since it instruments the app kit too).
|
|||
| 8 | **Registries and `host.root`** | registries, `self.writable`, `retryOn`, Durable's `registryReader`, `host.root` (`chord(fn, context)` and the harness object are p3's base code) |
|
||||
| 7a | **The dependency model** | requests that outlive providers, parked attempts without timers, wakes, the publication gate, getters, registries as dependencies, `parked_long` (`slice-7a.md`, `dependencies/model.md`) |
|
||||
| 7b | **Reload in the kernel** | `host.reload`: staging, commit, rollback; swaps, restarts, removal and re-serving; holds and `accepts` across a reload; jobs and registries across a reload; per-name wakes (`slice-7b/` brief) |
|
||||
| 7c | **Loader and dev** | `chord/next/loader` (discovery, `buildFacet`, `evaluateFacet`, `pluginFile`, dependencies) and `chord/next/dev` (watcher, reload control across hosts) |
|
||||
| 7c | **Plugins** | `chord/next/plugins` (discovery, `buildFacet`, `evaluateFacet`, `pluginFile`, `installDependencies`, the watcher, `rollout`, `mapStack`; no dev/prod split; the kernel's `load`/`reload` per plugin; `slice-7c.md`) |
|
||||
| 10 | **HTTP and workers** | `chord/next/http` (server, `Http` registry, responses as descriptions, WebSocket entry) and `chord/next/workers` (a worker per address, IPC, idle stop) |
|
||||
| 11 | **Web and TUI** | `chord/next/web` (page host, React hooks) and `chord/next/tui` (launcher, sandbox), from p2's code |
|
||||
| 9 | **Telemetry** | spans, events, metrics across hosts |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue