@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/CHANGELOG.md +16 -0
- package/README.md +144 -0
- package/package.json +15 -10
- package/src/effect.ts +2 -33
- package/src/index.ts +27 -36
- package/src/kernel.test.ts +44 -0
- package/src/kernel.ts +210 -241
- package/src/message.ts +37 -0
- package/src/plugin.ts +167 -185
- 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,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
|
-
*
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
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
|
|
117
|
+
/* Type-level plumbing */
|
|
110
118
|
/* ------------------------------------------------------------------ */
|
|
111
119
|
|
|
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
|
|
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
|
-
|
|
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
|
|
178
|
-
type MismatchedKeys<TDeclared
|
|
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
|
-
?
|
|
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
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|