summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/contracts/hooks.ts
blob: eb944650fb5bbc3c0d7597edf639a29367f28110 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
 * Hooks and services — typed cross-extension coupling anchors.
 *
 * Every cross-extension reference is anchored to an exported typed symbol
 * (not a raw string), so `lsp references` can compute the true blast radius
 * of a change. The id string lives in exactly one place: the owner's
 * `defineHook` / `defineFilter` / `defineService` declaration.
 *
 * Two hook kinds:
 * - **Event**: fire-and-forget, N listeners, error-isolated per handler.
 * - **Filter**: ordered value-in→value-out chain, in-band, awaited.
 *
 * Services are NOT hooks — they are single-responder request/response.
 */

/**
 * A typed descriptor for an event hook. The id string is the single source
 * of truth; consumers import this symbol, never the raw string.
 *
 * Event hooks are fire-and-forget: N listeners, errors isolated per handler
 * (a thrown handler is caught and logged — it never breaks the turn).
 */
export interface EventHookDescriptor<TPayload> {
	readonly kind: "event";
	readonly id: string;
	readonly _payload?: TPayload;
}

/**
 * A typed descriptor for a filter hook. Filters are an ordered chain:
 * each receives the current value and returns the (possibly transformed)
 * next value. Awaited in-band — a slow filter slows the turn, by design.
 *
 * Fail-open by default (a thrown filter logs and passes the value through);
 * the owner may mark a chain fail-closed.
 */
export interface FilterDescriptor<TValue> {
	readonly kind: "filter";
	readonly id: string;
	readonly _value?: TValue;
}

/** Union of hook descriptor kinds the kernel mechanism supports. */
export type HookDescriptor<TPayload> = EventHookDescriptor<TPayload> | FilterDescriptor<TPayload>;

/**
 * A typed service handle. Services are single-responder request/response —
 * NOT hooks. Modeling "ask the human for permission" as a hook invites
 * "which of N handlers wins?" ambiguity; a service has exactly one provider.
 */
export interface ServiceHandle<T> {
	readonly kind: "service";
	readonly id: string;
	readonly _type?: T;
}

/**
 * Create a typed event hook descriptor. Pure function, no side effects.
 * The returned object is the symbol consumers import and pass to `host.on`.
 *
 * @param id - Namespaced hook id in `owner/name` form (e.g. "kernel/turn.sealed").
 */
export function defineEventHook<TPayload>(id: string): EventHookDescriptor<TPayload> {
	return { kind: "event", id };
}

/**
 * Create a typed filter hook descriptor. Pure function, no side effects.
 * The returned object is the symbol consumers import and pass to `host.addFilter`.
 *
 * @param id - Namespaced filter id in `owner/name` form.
 */
export function defineFilter<TValue>(id: string): FilterDescriptor<TValue> {
	return { kind: "filter", id };
}

/**
 * Create a typed service handle. Pure function, no side effects.
 * The returned object is the symbol a provider passes to `host.provideService`
 * and consumers pass to `host.getService`.
 *
 * @param id - Namespaced service id in `owner/name` form.
 */
export function defineService<T>(id: string): ServiceHandle<T> {
	return { kind: "service", id };
}

/** Handler function for an event hook subscription. */
export type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;

/** Transform function for a filter hook in the chain. */
export type FilterHandler<TValue> = (value: TValue) => TValue | Promise<TValue>;