first commit
This commit is contained in:
120
docs/devtools/tooling/installer/src/host/apply/apply.md
Normal file
120
docs/devtools/tooling/installer/src/host/apply/apply.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# `apply`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - conflict — Resolves the per-op `conflict` policy on…
|
||||
- [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP…
|
||||
- [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave…
|
||||
- [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/…
|
||||
- conflict — Resolves the per-op `conflict` policy on…
|
||||
|
||||
- [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP…
|
||||
- [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave…
|
||||
- [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/…
|
||||
|
||||
- conflict — Resolves the per-op `conflict` policy on…
|
||||
|
||||
- [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP…
|
||||
- [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave…
|
||||
- [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/…
|
||||
|
||||
- conflict — Resolves the per-op `conflict` policy on…
|
||||
|
||||
- [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP…
|
||||
- [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave…
|
||||
- [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/…
|
||||
- [conflict](./conflict.md) — Resolves the per-op `conflict` policy on a target that already exists. Five policies are supported: `error` (raise `APP…
|
||||
- [json-merge](./json-merge.md) — Implements the deep-merge semantics used by `patch-json` ops with `conflict: "merge"`. Match the conventions of `@suave…
|
||||
- [text-patch](./text-patch.md) — Applies an ordered sequence of `TextEdit`s to a string. Used by `patch-text` ops to make targeted insertions/deletions/…
|
||||
|
||||
## Purpose
|
||||
|
||||
Materialises a `FileOp[]` against a `MutableInstallerFs` with rollback
|
||||
semantics. This is the only mutating stage in the installer pipeline.
|
||||
Each successful op records an `undo` entry on a journal; if any op fails,
|
||||
the journal replays in LIFO order to reverse already-applied work. Apply
|
||||
is wrapped in an OTel span (`installer.apply`) and is the only place in
|
||||
the host where `INSTALLER.APPLY_*` and `INSTALLER.ROLLBACK_FAILED` codes
|
||||
originate.
|
||||
|
||||
## Features
|
||||
|
||||
- Per-op conflict resolution via `resolveConflict` — the host consults
|
||||
each op's `conflict` policy before mutating the fs.
|
||||
- Transactional rollback in reverse order — an apply failure undoes
|
||||
everything that succeeded earlier in the run.
|
||||
- Distinct codes: `APPLY_CONFLICT` for policy violations, `APPLY_FAILED`
|
||||
for plugin/fs exceptions, `ROLLBACK_FAILED` when undo itself throws,
|
||||
`ABORTED` when the run was cancelled mid-apply.
|
||||
- `dry-run` is honoured by the host _before_ apply runs, so apply itself
|
||||
always writes — no internal `if (dryRun) skip` branches to test.
|
||||
- Per-op summary (`written`/`patched`/`skipped`/`conflicts`) for the host
|
||||
to return.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { applyOps, MemoryFs } from "@suaveplan/installer";
|
||||
|
||||
const fs = new MemoryFs();
|
||||
const summary = await applyOps({
|
||||
ops: [
|
||||
{
|
||||
kind: "write",
|
||||
path: "/hello.txt",
|
||||
contents: new TextEncoder().encode("hi"),
|
||||
},
|
||||
],
|
||||
ctx,
|
||||
fs,
|
||||
pluginId: "demo",
|
||||
pluginVersion: "0.1.0",
|
||||
});
|
||||
console.log(summary); // { written: 1, patched: 0, skipped: 0, conflicts: 0 }
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { applyOps, InstallerError } from "@suaveplan/installer";
|
||||
|
||||
try {
|
||||
await applyOps({ ops, ctx, fs, pluginId, pluginVersion });
|
||||
} catch (err) {
|
||||
if (err instanceof InstallerError) {
|
||||
switch (err.code) {
|
||||
case "INSTALLER.APPLY_CONFLICT":
|
||||
console.error("Pre-existing files conflict; re-run with --force");
|
||||
break;
|
||||
case "INSTALLER.ROLLBACK_FAILED":
|
||||
console.error(
|
||||
"Apply failed AND rollback failed — fs may be in a partial state",
|
||||
err.context?.["original"]
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `applyOps(options: ApplyOptions): Promise<PlanSummary>`
|
||||
- `interface ApplyOptions` —
|
||||
`{ ops, ctx, fs, pluginId, pluginVersion }`.
|
||||
- Errors: `INSTALLER.APPLY_CONFLICT`, `INSTALLER.APPLY_FAILED`,
|
||||
`INSTALLER.ROLLBACK_FAILED`, `INSTALLER.ABORTED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Each op's undo is captured into the journal _before_ the next op runs, so
|
||||
even if abort fires mid-op the partial state is recoverable. `mkdir` ops
|
||||
are short-circuited by `resolveConflict` for already-existing directories
|
||||
(idempotent default), so we don't need a redundant `if (exists) return`
|
||||
inside apply. The patch ops (`patch-json`, `patch-text`) read the prior
|
||||
content (when the file exists) so undo can restore byte-for-byte; when
|
||||
the file didn't exist, undo is a `deleteFile`. The `ROLLBACK_FAILED`
|
||||
path attaches the original apply error as `context.original` so the
|
||||
caller can still report root cause even when the recovery path failed.
|
||||
Reference in New Issue
Block a user