Files
punktfunk/plugin-kit/src/reconcile.ts
T
enricobuehler 10a0ef3283 style(plugin-kit): adopt the biome config its own plugins already use
The kit had NO biome config and no lint script, while every plugin repo that
consumes it has both. So its source quietly drifted — unused imports, unsorted
imports, formatting — with nothing to catch any of it. Running biome here for
the first time reported 20 findings across 8 files.

Adds `plugin-kit/biome.json` mirroring the plugin repos' (tab indent, double
quotes, recommended lint preset, organizeImports), a `check` script, and
`@biomejs/biome` pinned to the same `^2.5.2` the plugins pin — without that pin
`bunx biome` resolved 2.4.6, which rejects the 2.5 `rules.preset` key.

Two deliberate differences from the plugin repos' copy:

  * no `vcs.useIgnoreFile` — those are standalone repos with a .gitignore beside
    the config; plugin-kit is a directory inside this one, and biome errors with
    "couldn't find an ignore file". The `files.includes` exclusions cover it.
  * `!examples/**/dist` instead of `!ui/dist` — the kit has examples, not a UI.

`css.parser.tailwindDirectives` is carried over and is load-bearing: without it
biome cannot parse `@theme` in src/theme.css and reports three parse errors on
CSS that is perfectly valid Tailwind v4.

Everything here is formatter/import churn except two real findings, both fixed:

  * `Layer` (library/define.ts) and `Cause` (sync-engine.ts) were imported and
    never used;
  * test/spike-httpapi.test.ts read `(reg?.body as …).ui.secret` one line after
    `expect(reg).toBeDefined()`. The optional chain undoes the assertion: had
    `reg` been undefined the `.ui` access would throw a TypeError instead of
    failing the test readably. Now asserted to the type system too.

Wired into plugin-kit-publish.yml as a `Lint & format` step ahead of Typecheck,
so this cannot rot again.

Gates after: biome clean (42 files), tsc clean, 67/67 tests, build clean.
2026-08-08 02:19:06 +02:00

83 lines
3.0 KiB
TypeScript

// The library-provider client, owned by the kit so plugins stop hand-copying host calls.
// The wire SCHEMAS live in ./wire.ts (browser-safe — plugin contracts re-use them); the
// transport here stays the SDK's untyped `pf.request` seam (version-skew-safe under the
// runner-bundled SDK — design D7).
import { Context, Effect, Layer } from "effect";
import type { HostRequestError } from "./errors.js";
import { HostClient } from "./host-client.js";
import type { ProviderEntry } from "./wire.js";
export * from "./wire.js";
/** What the host echoed back for one reconciled entry — enough to tell whether a claim took. */
export interface ReconciledEntry {
readonly id: string;
readonly external_id?: string;
/** The store badge the host assigned: the claim when it honoured one, else `"custom"`. */
readonly store?: string;
}
export interface ProviderClientService {
/**
* Full-replace reconcile: PUT the desired set; the host diffs by `external_id`.
*
* `store` claims that store for this provider (design D2), which is what makes the entries carry
* the store's own identity — deterministic `<store>:<external_id>` ids instead of opaque
* `custom:<id>` ones, the store's badge, and suppression of the host's matching built-in scanner
* so the two never double-list. One provider per store: a second claimant gets a 409.
*
* Returns the host's echoed entries so a caller can verify the claim actually took — a host
* predating claims ignores the query parameter silently, and the only way to notice is that the
* entries come back as `custom`.
*/
readonly reconcile: (
providerId: string,
entries: ReadonlyArray<ProviderEntry>,
store?: string,
) => Effect.Effect<ReadonlyArray<ReconciledEntry>, HostRequestError>;
/**
* Remove every entry this provider owns **and release its store claim** (the explicit-uninstall
* path). Releasing is what brings the host's built-in scanner back.
*/
readonly remove: (
providerId: string,
) => Effect.Effect<void, HostRequestError>;
}
export class ProviderClient extends Context.Service<
ProviderClient,
ProviderClientService
>()("@punktfunk/plugin-kit/ProviderClient") {
static readonly layer: Layer.Layer<ProviderClient, never, HostClient> =
Layer.effect(ProviderClient)(
Effect.gen(function* () {
const host = yield* HostClient;
return {
reconcile: (providerId, entries, store) =>
host
.request(
"PUT",
`/library/provider/${providerId}${
store ? `?store=${encodeURIComponent(store)}` : ""
}`,
entries,
)
.pipe(
// The host answers with its resulting entries. An older host may answer
// with something else, so treat a non-array as "no echo" rather than
// failing the sync.
Effect.map((body) =>
Array.isArray(body)
? (body as ReadonlyArray<ReconciledEntry>)
: [],
),
),
remove: (providerId) =>
host
.request("DELETE", `/library/provider/${providerId}`)
.pipe(Effect.asVoid),
} satisfies ProviderClientService;
}),
);
}