@lambdot/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.ts CHANGED
@@ -1,225 +1,207 @@
1
- import type {
2
- Address,
3
- ContextView,
4
- InputContext,
5
- OutputContract,
6
- OutputContractMap,
7
- } from "./context.ts";
8
- import type { Effect } from "./effect.ts";
9
- import type { EventMap } from "./events.ts";
1
+ import type { Disposer } from "./effect.ts";
10
2
  import type { StandardSchemaV1 } from "./schema.ts";
11
3
 
12
4
  /**
13
- * The `inject` requirement: once a plugin declares typed capability needs
14
- * (`TInjects`), the runtime `inject` array is restricted to exactly those
15
- * names the runtime gate and the type-level gate cannot drift apart.
16
- * Plugins with no declared needs keep the loose string form (the
17
- * runtime-only capability path, e.g. `inject: ["state"]`).
5
+ * Per-activation services handed to a plugin's `apply`: collect disposers
6
+ * (run when the owning plugin unloads, in reverse order) and report errors
7
+ * from background work (stream pumps, timers).
18
8
  */
19
- type InjectNames<TInjects extends object> = [keyof TInjects & string] extends [never]
20
- ? readonly string[]
21
- : readonly (keyof TInjects & string)[];
22
-
23
- /** Metadata understood by the kernel, shared by all plugin roles. */
24
- export interface PluginMeta<TConfig, TInjects extends object = {}> {
25
- readonly name: string;
26
- /** Capabilities that must be provided before this plugin activates. */
27
- readonly inject?: InjectNames<TInjects>;
28
- /**
29
- * Capability names this plugin provides once active. Provided valueless
30
- * by the kernel after `apply`; to provide a typed value, declare
31
- * `TProvides` and call `ctx.provide(name, value)` in `apply` instead.
32
- */
33
- readonly provide?: string | readonly string[];
34
- /** Standard-Schema validator applied to config before `apply` runs. */
9
+ export interface Scope {
10
+ onDispose(disposer: Disposer): void;
11
+ onError(error: unknown): void;
12
+ }
13
+
14
+ /**
15
+ * The author-facing half of a plugin: a name (its namespace key once
16
+ * composed), an optional Standard-Schema config validator, and `apply`
17
+ * the function from the plugin's declared input to its emitted output.
18
+ */
19
+ export interface PluginSpec<TIn, TOut, TConfig, TName extends string> {
20
+ readonly name: TName;
35
21
  readonly Config?: StandardSchemaV1<unknown, TConfig>;
22
+ apply(input: TIn, scope: Scope, config: TConfig): TOut | Promise<TOut>;
36
23
  }
37
24
 
38
25
  /**
39
- * Produces events of the kinds in `TEvents`. Knows how to listen, nothing
40
- * else. May declare typed capabilities it provides (`TProvides`) and
41
- * consumes (`TInjects` folded into its `apply` context, gated at `use()`).
26
+ * A plugin is a function: `apply` maps its declared input (`TIn`, a record
27
+ * of namespaces it consumes) to its output (`TOut`, the value it emits).
28
+ * Composition methods (`use`/`bind`) build bigger plugins out of smaller
29
+ * ones; `start`/`stop`/`ctx` make any plugin runnable on its own.
42
30
  */
43
- export interface InputPlugin<
44
- TEvents extends EventMap = EventMap,
45
- TConfig = void,
46
- TName extends string = string,
47
- TProvides extends object = {},
48
- TInjects extends object = {},
49
- > extends PluginMeta<TConfig, TInjects> {
50
- readonly role: "input";
31
+ export interface Plugin<TIn = void, TOut = unknown, TConfig = void, TName extends string = string> {
51
32
  readonly name: TName;
52
- apply(ctx: InputContext<TEvents, TProvides> & TInjects, config: TConfig): Effect;
33
+ readonly Config?: StandardSchemaV1<unknown, TConfig>;
34
+ apply(input: TIn, scope: Scope, config: TConfig): TOut | Promise<TOut>;
35
+
36
+ /** Feed `unit` from the visible context and expose its output under `as` (default: its name). */
37
+ use<const TUnit extends AnyUnit, TAs extends string = NameOf<TUnit>>(
38
+ unit: TUnit & FreshName<TAs, { [K in TName]: TOut }>,
39
+ ...args: WireArgs<TUnit, InputPart<TIn> & { [K in TName]: TOut }, TAs>
40
+ ): Composite<TIn, { [K in TName]: TOut } & { [K in TAs]: OutOf<TUnit> }, {}, TName>;
41
+
42
+ /** Like {@link use}, but the unit's output stays internal to the composition. */
43
+ bind<const TUnit extends AnyUnit, TAs extends string = NameOf<TUnit>>(
44
+ unit: TUnit & FreshName<TAs, { [K in TName]: TOut }>,
45
+ ...args: WireArgs<TUnit, InputPart<TIn> & { [K in TName]: TOut }, TAs>
46
+ ): Composite<TIn, { [K in TName]: TOut }, { [K in TAs]: OutOf<TUnit> }, TName>;
47
+
48
+ start(...args: StartArgs<TIn>): Promise<void>;
49
+ stop(): Promise<void>;
50
+ /** The exposed namespaces. Populated during `start`; typed regardless. */
51
+ readonly ctx: { [K in TName]: TOut };
53
52
  }
54
53
 
55
- /** Consumes addresses of its platform. Reply semantics live in `TContent`, not the core. */
56
- export interface OutputPlugin<
57
- TPlatform extends string = string,
58
- TAddress extends Address<TPlatform> = Address<TPlatform>,
59
- TContent = unknown,
60
- TConfig = void,
61
- TName extends string = string,
62
- TProvides extends object = {},
63
- TInjects extends object = {},
64
- > extends PluginMeta<TConfig, TInjects> {
65
- readonly role: "output";
54
+ /**
55
+ * A composed chain of plugins — itself wireable like a plugin: its input is
56
+ * the chain's external requirement, its output is the visible context
57
+ * (`TVisible`). `THidden` carries the `bind`-encapsulated namespaces:
58
+ * visible to later `mapping`s inside the chain, absent from the final `ctx`.
59
+ * Structural sibling of {@link Plugin} (the conditional wire types do not
60
+ * survive interface extension).
61
+ */
62
+ export interface Composite<TIn = void, TVisible = {}, THidden = {}, TName extends string = string> {
66
63
  readonly name: TName;
67
- readonly platform: TPlatform;
68
- send(to: TAddress, content: TContent): void | Promise<void>;
69
- apply?(ctx: ContextView<{}, {}, {}, TProvides> & TInjects, config: TConfig): Effect;
64
+ apply(input: TIn, scope: Scope, config: void): Promise<TVisible>;
65
+
66
+ use<const TUnit extends AnyUnit, TAs extends string = NameOf<TUnit>>(
67
+ unit: TUnit & FreshName<TAs, TVisible & THidden>,
68
+ ...args: WireArgs<TUnit, InputPart<TIn> & TVisible & THidden, TAs>
69
+ ): Composite<TIn, TVisible & { [K in TAs]: OutOf<TUnit> }, THidden, TName>;
70
+
71
+ bind<const TUnit extends AnyUnit, TAs extends string = NameOf<TUnit>>(
72
+ unit: TUnit & FreshName<TAs, TVisible & THidden>,
73
+ ...args: WireArgs<TUnit, InputPart<TIn> & TVisible & THidden, TAs>
74
+ ): Composite<TIn, TVisible, THidden & { [K in TAs]: OutOf<TUnit> }, TName>;
75
+
76
+ /**
77
+ * Seal the chain into a final artifact under a new name. The engine's
78
+ * type is exactly `Engine<TIn, TVisible, TAlias>`: the external input
79
+ * requirement is preserved, the `bind`-encapsulated internals
80
+ * (`THidden`) and the chain's own name are erased. Composing onto an
81
+ * exposed chain throws at runtime.
82
+ */
83
+ expose<const TAlias extends string>(name: TAlias): Engine<TIn, TVisible, TAlias>;
84
+
85
+ start(...args: StartArgs<TIn>): Promise<void>;
86
+ stop(): Promise<void>;
87
+ readonly ctx: TVisible;
70
88
  }
71
89
 
72
90
  /**
73
- * A unit of behavior. Declares the event kinds it handles (`TNeeds`), the
74
- * output platforms it sends through (`TSends`), and optionally a state
75
- * schema (`TStateSchema`) and typed capabilities: `TProvides` (read back
76
- * through `CapsOf` at the kernel fold; `provide` is type-checked against
77
- * it) and `TInjects` (folded into the `apply` context; the kernel checks
78
- * at `use()` time that the fold so far provides them).
91
+ * A sealed composition what {@link Composite.expose} returns. Structurally
92
+ * a unit (`name` + `apply`), so it wires into a supervisor kernel like any
93
+ * plugin, but the composition methods are gone: an engine is final. Its
94
+ * output is the chain's visible context; its input is the chain's external
95
+ * requirement. Internal (`bind`ed) namespaces exist only at runtime, never
96
+ * in the type.
79
97
  */
80
- export interface FeaturePlugin<
81
- TNeeds extends EventMap = {},
82
- TSends extends OutputContractMap = {},
83
- TStateSchema = undefined,
84
- TConfig = void,
85
- TName extends string = string,
86
- TProvides extends object = {},
87
- TInjects extends object = {},
88
- > extends PluginMeta<TConfig, TInjects> {
89
- readonly role?: "feature";
98
+ export interface Engine<TIn = void, TOut = {}, TName extends string = string> {
90
99
  readonly name: TName;
91
- apply(
92
- ctx: ContextView<
93
- TNeeds,
94
- TSends,
95
- TStateSchema extends undefined ? {} : { [K in TName]: TStateSchema },
96
- TProvides
97
- > &
98
- TInjects,
99
- config: TConfig,
100
- ): Effect;
100
+ apply(input: TIn, scope: Scope, config: void): Promise<TOut>;
101
+
102
+ start(...args: StartArgs<TIn>): Promise<void>;
103
+ stop(): Promise<void>;
104
+ /** The exposed namespaces. Populated during `start`; typed regardless. */
105
+ readonly ctx: TOut;
101
106
  }
102
107
 
103
- export type AnyPlugin =
104
- | InputPlugin<any, any, any, any, any>
105
- | OutputPlugin<any, any, any, any, any, any, any>
106
- | FeaturePlugin<any, any, any, any, any, any, any>;
108
+ export type AnyUnit =
109
+ | Plugin<any, any, any, any>
110
+ | Composite<any, any, any, any>
111
+ | Engine<any, any, any>;
112
+
113
+ /** The kernel is a composition seeded empty: `createKernel().use(...)`. */
114
+ export type Kernel<TVisible = {}, THidden = {}> = Composite<void, TVisible, THidden, "kernel">;
107
115
 
108
116
  /* ------------------------------------------------------------------ */
109
- /* Type-level fold: how each plugin shapes the kernel's type params */
117
+ /* Type-level plumbing */
110
118
  /* ------------------------------------------------------------------ */
111
119
 
112
- export type EventsOf<TPlugin> =
113
- TPlugin extends InputPlugin<infer TEvents, any, any, any, any> ? TEvents : {};
114
-
115
- export type OutputsOf<TPlugin> =
116
- TPlugin extends OutputPlugin<
117
- infer TPlatform,
118
- infer TAddress,
119
- infer TContent,
120
- any,
121
- any,
122
- any,
123
- any
124
- >
125
- ? { [K in TPlatform]: OutputContract<TAddress, TContent> }
126
- : {};
127
-
128
- export type StateOf<TPlugin> =
129
- TPlugin extends FeaturePlugin<any, any, infer TStateSchema, any, infer TName, any, any>
130
- ? TStateSchema extends undefined
131
- ? {}
132
- : { [K in TName]: TStateSchema }
133
- : {};
134
-
135
- /** Typed capabilities a plugin provides, folded into the kernel's `TCaps`. */
136
- export type CapsOf<TPlugin> =
137
- TPlugin extends FeaturePlugin<any, any, any, any, any, infer TProvides, any>
138
- ? TProvides
139
- : TPlugin extends InputPlugin<any, any, any, infer TProvides, any>
140
- ? TProvides
141
- : TPlugin extends OutputPlugin<any, any, any, any, any, infer TProvides, any>
142
- ? TProvides
143
- : {};
144
-
145
- /** Typed capabilities a plugin consumes, gated against the fold at `use()`. */
146
- export type InjectsOf<TPlugin> =
147
- TPlugin extends FeaturePlugin<any, any, any, any, any, any, infer TInjects>
148
- ? TInjects
149
- : TPlugin extends InputPlugin<any, any, any, any, infer TInjects>
150
- ? TInjects
151
- : TPlugin extends OutputPlugin<any, any, any, any, any, any, infer TInjects>
152
- ? TInjects
153
- : {};
154
-
155
- /** Config type: from the schema if present, else from `apply`'s second parameter. */
156
- export type ConfigOf<TPlugin> = TPlugin extends { Config: StandardSchemaV1<any, infer TOutput> }
157
- ? TOutput
158
- : TPlugin extends { apply: (ctx: any, config: infer TConfig) => any }
159
- ? unknown extends TConfig
120
+ /**
121
+ * Inference helpers work structurally over `apply`, so they read both leaf
122
+ * plugins and composites (which carry no declared config — `void`).
123
+ */
124
+ export type InOf<TUnit> = TUnit extends {
125
+ apply(input: infer TIn, scope: any, config: any): any;
126
+ }
127
+ ? TIn
128
+ : never;
129
+ export type OutOf<TUnit> = TUnit extends {
130
+ apply(input: any, scope: any, config: any): infer TOut;
131
+ }
132
+ ? Awaited<TOut>
133
+ : never;
134
+ export type ConfigOf<TUnit> = TUnit extends { Config?: StandardSchemaV1<any, infer TConfig> }
135
+ ? [TConfig] extends [void]
136
+ ? void
137
+ : TConfig
138
+ : TUnit extends { apply(input: any, scope: any, config: infer TConfig): any }
139
+ ? [TConfig] extends [void]
160
140
  ? void
161
141
  : TConfig
162
142
  : void;
143
+ export type NameOf<TUnit> = TUnit extends { readonly name: infer TName extends string }
144
+ ? TName
145
+ : never;
163
146
 
164
- /** Makes the config argument optional exactly when `void` is assignable to it. */
165
- export type Spread<T> = [T] extends [void] ? [config?: T] : [config: T];
166
-
167
- type NeedsOf<TPlugin> =
168
- TPlugin extends FeaturePlugin<infer TNeeds, any, any, any, any, any, any> ? TNeeds : {};
169
- type SendsOf<TPlugin> =
170
- TPlugin extends FeaturePlugin<any, infer TSends, any, any, any, any, any> ? TSends : {};
171
-
172
- type MissingKeys<TDeclared extends object, TRegistered extends object> = Exclude<
147
+ type MissingKeys<TDeclared, TRegistered> = Exclude<
173
148
  keyof TDeclared & string,
174
149
  keyof TRegistered & string
175
150
  >;
176
151
 
177
- /** Declared capability keys whose value types don't match the folded capability. */
178
- type MismatchedKeys<TDeclared extends object, TRegistered extends object> = {
152
+ /** Declared input keys whose value types don't match the visible context. */
153
+ type MismatchedKeys<TDeclared, TRegistered> = {
179
154
  [K in keyof TDeclared & string]: K extends keyof TRegistered & string
180
- ? TDeclared[K] extends TRegistered[K]
155
+ ? TRegistered[K] extends TDeclared[K]
181
156
  ? never
182
157
  : K
183
158
  : never;
184
159
  }[keyof TDeclared & string];
185
160
 
186
161
  /**
187
- * Compile-time gate on `use()`: a plugin can only be registered once every
188
- * event kind it handles, every output platform it sends through, and every
189
- * typed capability it injects is already in the fold — with a compatible
190
- * value type.
162
+ * Whether identity wiring (no `mapping`) can feed a unit expecting `TIn`
163
+ * from the currently visible context: every declared key present, with a
164
+ * compatible value type.
191
165
  */
192
- export type Validate<
193
- TPlugin,
194
- TEvents extends EventMap,
195
- TOutputs extends OutputContractMap,
196
- TCaps extends object,
197
- > = [MissingKeys<NeedsOf<TPlugin>, TEvents>] extends [never]
198
- ? [MissingKeys<SendsOf<TPlugin>, TOutputs>] extends [never]
199
- ? [MissingKeys<InjectsOf<TPlugin>, TCaps>] extends [never]
200
- ? [MismatchedKeys<InjectsOf<TPlugin>, TCaps>] extends [never]
201
- ? unknown
202
- : {
203
- readonly "mismatched capability types": MismatchedKeys<
204
- InjectsOf<TPlugin>,
205
- TCaps
206
- >;
207
- }
208
- : { readonly "unprovided capabilities": MissingKeys<InjectsOf<TPlugin>, TCaps> }
209
- : { readonly "unregistered output platforms": MissingKeys<SendsOf<TPlugin>, TOutputs> }
210
- : { readonly "unregistered event kinds": MissingKeys<NeedsOf<TPlugin>, TEvents> };
211
-
212
- /** Identity helper for authoring feature plugins with precise generics. */
213
- export function definePlugin<
214
- TNeeds extends EventMap = {},
215
- TSends extends OutputContractMap = {},
216
- TStateSchema = undefined,
217
- TConfig = void,
218
- TName extends string = string,
219
- TProvides extends object = {},
220
- TInjects extends object = {},
221
- >(
222
- plugin: FeaturePlugin<TNeeds, TSends, TStateSchema, TConfig, TName, TProvides, TInjects>,
223
- ): typeof plugin {
224
- return plugin;
225
- }
166
+ type Satisfied<TIn, TAvailable> = [keyof TIn & string] extends [never]
167
+ ? true
168
+ : [MissingKeys<TIn, TAvailable>] extends [never]
169
+ ? [MismatchedKeys<TIn, TAvailable>] extends [never]
170
+ ? true
171
+ : false
172
+ : false;
173
+
174
+ /** Compile error marker when a namespace key is already taken in the chain. */
175
+ type FreshName<TName extends string, TTaken> = TName extends keyof TTaken & string
176
+ ? { readonly "duplicate namespace": TName }
177
+ : unknown;
178
+
179
+ /** `void` inputs contribute nothing to the mapping's context type. */
180
+ type InputPart<TIn> = [TIn] extends [void] ? {} : TIn;
181
+
182
+ /** Adds `option` to the wire options, required exactly when the unit's config is non-void. */
183
+ type WithOption<TUnit, TBase extends object> = [ConfigOf<TUnit>] extends [void]
184
+ ? TBase & { option?: ConfigOf<TUnit> }
185
+ : TBase & { option: ConfigOf<TUnit> };
186
+
187
+ /**
188
+ * The trailing arguments of `use`/`bind`. `mapping` rewires the visible
189
+ * context into the unit's declared input — required when identity wiring
190
+ * cannot satisfy it, optional otherwise. `option` carries the unit's
191
+ * config, required exactly when the config type is non-void. `as` renames
192
+ * the namespace the unit's output is stored under.
193
+ */
194
+ export type WireArgs<TUnit extends AnyUnit, TAvailable, TAs extends string> =
195
+ Satisfied<InOf<TUnit>, TAvailable> extends true
196
+ ? [ConfigOf<TUnit>] extends [void]
197
+ ? [
198
+ options?: WithOption<
199
+ TUnit,
200
+ { mapping?: (ctx: TAvailable) => InOf<TUnit>; as?: TAs }
201
+ >,
202
+ ]
203
+ : [options: WithOption<TUnit, { mapping?: (ctx: TAvailable) => InOf<TUnit>; as?: TAs }>]
204
+ : [options: WithOption<TUnit, { mapping: (ctx: TAvailable) => InOf<TUnit>; as?: TAs }>];
205
+
206
+ /** `start` takes the composition's external input exactly when it declares one. */
207
+ export type StartArgs<TIn> = [TIn] extends [void] ? [] : [input: TIn];
package/src/state.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Pluggable state. The framework core is stateless; state is an ordinary
3
- * feature plugin providing its backend as the `"state"` capability (at most
4
- * one may be active). Plugins declare their schema at the type level and
5
- * receive a typed accessor namespaced to their plugin name.
2
+ * Pluggable state. The framework core is stateless; a state plugin is an
3
+ * ordinary plugin emitting a backend as its namespace value. Stateful
4
+ * features declare the backend in their input and build a typed accessor
5
+ * namespaced to their own plugin name via {@link createStateAccessor}.
6
6
  */
7
7
  export interface StateBackend {
8
8
  get(namespace: string, key: string): Promise<unknown>;
@@ -21,25 +21,6 @@ export interface StateAccessor<TSchema> {
21
21
  delete(key: keyof TSchema & string): Promise<void>;
22
22
  }
23
23
 
24
- /**
25
- * Marker type for `ctx.state` when no plugin declared a state schema —
26
- * "stateless by default" is enforced at compile time. Any access errors with
27
- * this type's name in the message.
28
- */
29
- export interface NoStateDeclared {
30
- readonly "no state schema declared": "register a feature plugin with a TStateSchema to enable ctx.state";
31
- }
32
-
33
- /**
34
- * `ctx.state`. Folds to {@link NoStateDeclared} when no plugin declared a
35
- * state schema.
36
- */
37
- export type StateView<TState extends object> = keyof TState extends never
38
- ? NoStateDeclared
39
- : {
40
- for<TName extends keyof TState & string>(plugin: TName): StateAccessor<TState[TName]>;
41
- };
42
-
43
24
  export function createStateAccessor<TSchema>(
44
25
  backend: StateBackend,
45
26
  namespace: string,