@primafuture/contrib-kit-core
v1.2.0
Published
Framework-agnostic extension points and contribution registry with deterministic lifecycle orchestration.
Maintainers
Readme
@primafuture/contrib-kit-core
Framework-agnostic extension points and a contribution registry with deterministic activation, replacement, cleanup, and teardown ordering.
Installation
pnpm add @primafuture/contrib-kit-coreContrib Kit Core is ESM-only and has no framework or runtime dependencies. Import the package root; internal and source subpaths are intentionally not exported.
Basic registry flow
import * as contribKitCore from "@primafuture/contrib-kit-core";
interface MenuContext {
readonly includeAdmin: boolean;
}
interface MenuItemData {
readonly label: string;
readonly adminOnly: boolean;
}
interface MenuItemImplementation {
render(label: string): string;
}
const menuPoint = contribKitCore.defineExtensionPoint<
MenuContext,
MenuItemData,
MenuItemImplementation
>()({
pointId: "example/menu-items",
contractVersion: 1,
cardinality: "many",
filter(contribution, context): boolean {
return context.includeAdmin || !contribution.data.adminOnly;
},
selectionPolicy: {
select(contributions) {
return contributions.slice(0, 5);
},
},
});
const registry = contribKitCore.createExtensionRegistry();
const pointOwner = registry.rootScope.registerPoint(menuPoint);
const registration = registry.registerContribution(menuPoint, {
contributionId: "example/menu-items/home",
expectedContractVersion: 1,
data: { label: "Home", adminOnly: false },
implementation: {
render(label): string {
return label;
},
},
});
await registration.whenActive();
const catalog = registry.listContributions(menuPoint);
const visible = registry.resolveContributions(menuPoint, { includeAdmin: false });
console.log(catalog.length, visible[0]?.implementation.render(visible[0].data.label));
await registration.dispose();
await pointOwner.close().whenCleanupFinished();
await registry.dispose();Admission returns a pending handle and activation starts from the point-local FIFO in a later microtask. Call whenActive() only when the host needs the activation outcome. A rejected activation never enters the committed catalog.
Resolved collection policy
resolveContributions() applies one captured synchronous pipeline:
context validation → filter → ordering → selectionContributionSelectionPolicy sees the complete ordered view and returns an
order-preserving canonical subset. This is the right place for collective membership
rules such as limits, companion entries, exclusivity winners, or removal of orphaned
structural records.
Define selectionPolicy on the point for the shared default. A point owner may supply
a local override while registering a generation, and
pointOwner.updatePolicy({ selectionPolicy: null }) clears that override and restores
the latest definition default. Selection changes resolved membership only; they never
remove committed registrations from listContributions().
Dynamic point availability
A module can subscribe before another independently loaded module registers the point.
Core invokes onAvailable for every active generation and pairs it with at most one
onUnavailable when that generation closes:
const unsubscribe = registry.observePoint(menuPoint, {
onAvailable(): void {
const registration = registry.registerContribution(menuPoint, contributionInput);
void registration.whenActive();
},
onUnavailable(): void {
// Drop generation-local state; point close owns removal of its catalog.
},
});
// Detachment does not synthesize cleanup. Dispose module-owned handles first.
unsubscribe();Observation matches the exact canonical point reference, not only its pointId.
See the workspace dynamic-plugin example for explicit handle cleanup across provider
unload and reload.
Ownership and cleanup
- An owner scope owns its child scopes and registered point generations.
- A registration owns its activation resource;
dispose()is idempotent and waits for its captured cleanup outcome. pointOwner.close()removes the exact point generation and exposes separate closed and cleanup outcomes.registry.dispose()traverses the ownership tree in deterministic reverse order.- Registry events and snapshots expose committed scalar state. Contribution
data,implementation, callbacks, and controllers are not copied into snapshots.
The Core registry is host-owned. Framework adapters may observe or project it, but they must not silently dispose it.
License
ISC © 2026 PrimaFuture.cz s.r.o.
