@lambdot/core 0.1.0 → 0.1.1

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,177 @@
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. */
35
- readonly Config?: StandardSchemaV1<unknown, TConfig>;
9
+ export interface Scope {
10
+ onDispose(disposer: Disposer): void;
11
+ onError(error: unknown): void;
36
12
  }
37
13
 
38
14
  /**
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()`).
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.
42
18
  */
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";
19
+ export interface PluginSpec<TIn, TOut, TConfig, TName extends string> {
51
20
  readonly name: TName;
52
- apply(ctx: InputContext<TEvents, TProvides> & TInjects, config: TConfig): Effect;
21
+ readonly Config?: StandardSchemaV1<unknown, TConfig>;
22
+ apply(input: TIn, scope: Scope, config: TConfig): TOut | Promise<TOut>;
53
23
  }
54
24
 
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";
25
+ /**
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.
30
+ */
31
+ export interface Plugin<TIn = void, TOut = unknown, TConfig = void, TName extends string = string> {
66
32
  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;
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 };
70
52
  }
71
53
 
72
54
  /**
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).
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).
79
61
  */
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";
62
+ export interface Composite<TIn = void, TVisible = {}, THidden = {}, TName extends string = string> {
90
63
  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;
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
+ start(...args: StartArgs<TIn>): Promise<void>;
77
+ stop(): Promise<void>;
78
+ readonly ctx: TVisible;
101
79
  }
102
80
 
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>;
81
+ export type AnyUnit = Plugin<any, any, any, any> | Composite<any, any, any, any>;
82
+
83
+ /** The kernel is a composition seeded empty: `createKernel().use(...)`. */
84
+ export type Kernel<TVisible = {}, THidden = {}> = Composite<void, TVisible, THidden, "kernel">;
107
85
 
108
86
  /* ------------------------------------------------------------------ */
109
- /* Type-level fold: how each plugin shapes the kernel's type params */
87
+ /* Type-level plumbing */
110
88
  /* ------------------------------------------------------------------ */
111
89
 
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
90
+ /**
91
+ * Inference helpers work structurally over `apply`, so they read both leaf
92
+ * plugins and composites (which carry no declared config — `void`).
93
+ */
94
+ export type InOf<TUnit> = TUnit extends {
95
+ apply(input: infer TIn, scope: any, config: any): any;
96
+ }
97
+ ? TIn
98
+ : never;
99
+ export type OutOf<TUnit> = TUnit extends {
100
+ apply(input: any, scope: any, config: any): infer TOut;
101
+ }
102
+ ? Awaited<TOut>
103
+ : never;
104
+ export type ConfigOf<TUnit> = TUnit extends { Config?: StandardSchemaV1<any, infer TConfig> }
105
+ ? [TConfig] extends [void]
106
+ ? void
107
+ : TConfig
108
+ : TUnit extends { apply(input: any, scope: any, config: infer TConfig): any }
109
+ ? [TConfig] extends [void]
160
110
  ? void
161
111
  : TConfig
162
112
  : void;
113
+ export type NameOf<TUnit> = TUnit extends { readonly name: infer TName extends string }
114
+ ? TName
115
+ : never;
163
116
 
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<
117
+ type MissingKeys<TDeclared, TRegistered> = Exclude<
173
118
  keyof TDeclared & string,
174
119
  keyof TRegistered & string
175
120
  >;
176
121
 
177
- /** Declared capability keys whose value types don't match the folded capability. */
178
- type MismatchedKeys<TDeclared extends object, TRegistered extends object> = {
122
+ /** Declared input keys whose value types don't match the visible context. */
123
+ type MismatchedKeys<TDeclared, TRegistered> = {
179
124
  [K in keyof TDeclared & string]: K extends keyof TRegistered & string
180
- ? TDeclared[K] extends TRegistered[K]
125
+ ? TRegistered[K] extends TDeclared[K]
181
126
  ? never
182
127
  : K
183
128
  : never;
184
129
  }[keyof TDeclared & string];
185
130
 
186
131
  /**
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.
132
+ * Whether identity wiring (no `mapping`) can feed a unit expecting `TIn`
133
+ * from the currently visible context: every declared key present, with a
134
+ * compatible value type.
191
135
  */
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
- }
136
+ type Satisfied<TIn, TAvailable> = [keyof TIn & string] extends [never]
137
+ ? true
138
+ : [MissingKeys<TIn, TAvailable>] extends [never]
139
+ ? [MismatchedKeys<TIn, TAvailable>] extends [never]
140
+ ? true
141
+ : false
142
+ : false;
143
+
144
+ /** Compile error marker when a namespace key is already taken in the chain. */
145
+ type FreshName<TName extends string, TTaken> = TName extends keyof TTaken & string
146
+ ? { readonly "duplicate namespace": TName }
147
+ : unknown;
148
+
149
+ /** `void` inputs contribute nothing to the mapping's context type. */
150
+ type InputPart<TIn> = [TIn] extends [void] ? {} : TIn;
151
+
152
+ /** Adds `option` to the wire options, required exactly when the unit's config is non-void. */
153
+ type WithOption<TUnit, TBase extends object> = [ConfigOf<TUnit>] extends [void]
154
+ ? TBase & { option?: ConfigOf<TUnit> }
155
+ : TBase & { option: ConfigOf<TUnit> };
156
+
157
+ /**
158
+ * The trailing arguments of `use`/`bind`. `mapping` rewires the visible
159
+ * context into the unit's declared input — required when identity wiring
160
+ * cannot satisfy it, optional otherwise. `option` carries the unit's
161
+ * config, required exactly when the config type is non-void. `as` renames
162
+ * the namespace the unit's output is stored under.
163
+ */
164
+ export type WireArgs<TUnit extends AnyUnit, TAvailable, TAs extends string> =
165
+ Satisfied<InOf<TUnit>, TAvailable> extends true
166
+ ? [ConfigOf<TUnit>] extends [void]
167
+ ? [
168
+ options?: WithOption<
169
+ TUnit,
170
+ { mapping?: (ctx: TAvailable) => InOf<TUnit>; as?: TAs }
171
+ >,
172
+ ]
173
+ : [options: WithOption<TUnit, { mapping?: (ctx: TAvailable) => InOf<TUnit>; as?: TAs }>]
174
+ : [options: WithOption<TUnit, { mapping: (ctx: TAvailable) => InOf<TUnit>; as?: TAs }>];
175
+
176
+ /** `start` takes the composition's external input exactly when it declares one. */
177
+ 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,