@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/CHANGELOG.md +6 -0
- package/README.md +135 -0
- package/package.json +15 -10
- package/src/effect.ts +2 -33
- package/src/index.ts +26 -36
- package/src/kernel.ts +170 -249
- package/src/message.ts +37 -0
- package/src/plugin.ts +138 -186
- package/src/state.ts +4 -23
- package/src/stream.ts +281 -0
- package/src/context.ts +0 -120
- package/src/events.ts +0 -147
- package/src/fiber.ts +0 -41
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
|
-
*
|
|
14
|
-
* (
|
|
15
|
-
*
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
:
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
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
|
-
|
|
21
|
+
readonly Config?: StandardSchemaV1<unknown, TConfig>;
|
|
22
|
+
apply(input: TIn, scope: Scope, config: TConfig): TOut | Promise<TOut>;
|
|
53
23
|
}
|
|
54
24
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
87
|
+
/* Type-level plumbing */
|
|
110
88
|
/* ------------------------------------------------------------------ */
|
|
111
89
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
|
178
|
-
type MismatchedKeys<TDeclared
|
|
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
|
-
?
|
|
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
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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,
|