@lambdot/core 0.1.1 → 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 +10 -0
- package/README.md +10 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/kernel.test.ts +44 -0
- package/src/kernel.ts +48 -0
- package/src/plugin.ts +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#12](https://github.com/Embers-of-the-Fire/lambdot/pull/12) [`71e5732`](https://github.com/Embers-of-the-Fire/lambdot/commit/71e57321ad4ec7d1aef3651d104123f8167ec2e7) Thanks [@Embers-of-the-Fire](https://github.com/Embers-of-the-Fire)! - Add `Composite.expose(name)`: seal a kernel chain into a final, named `Engine` artifact. The engine preserves the chain's external input requirement, erases `bind`-encapsulated internals from its type, drops the composition methods (`use`/`bind` throw at runtime once exposed), and wires into a supervisor kernel under its new name.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#12](https://github.com/Embers-of-the-Fire/lambdot/pull/12) [`71e5732`](https://github.com/Embers-of-the-Fire/lambdot/commit/71e57321ad4ec7d1aef3651d104123f8167ec2e7) Thanks [@Embers-of-the-Fire](https://github.com/Embers-of-the-Fire)! - Implement `Engine.apply` on the runtime behind `Composite.expose`: the sealed engine now delegates to its inner composition, so calling `engine.apply(input, scope, config)` directly works instead of throwing `TypeError: engine.apply is not a function`.
|
|
12
|
+
|
|
3
13
|
## 0.1.1
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -48,6 +48,14 @@ as })` feeds a plugin from the namespaces visible so far and exposes its
|
|
|
48
48
|
composition order — resolve mapping, validate config, `apply` — and
|
|
49
49
|
`stop()` disposes in reverse. Ordering mistakes are compile errors in the
|
|
50
50
|
mappings, not runtime states.
|
|
51
|
+
- **`expose(name)` seals a chain into a final engine.** The engine is the
|
|
52
|
+
chain as an artifact: named, runnable (`start`/`stop`/`ctx`), and wireable
|
|
53
|
+
into a supervisor kernel under its new name — but no longer composable
|
|
54
|
+
(`use`/`bind` are gone from the type and throw at runtime). Its type is
|
|
55
|
+
exactly `Engine<TIn, TVisible, TName>`: the chain's external input
|
|
56
|
+
requirement survives, while the `bind`-encapsulated internals and the
|
|
57
|
+
chain's own name are erased. This is how N instances of one bot stack nest
|
|
58
|
+
into a supervisor without name tags or leaked internals.
|
|
51
59
|
|
|
52
60
|
## Usage
|
|
53
61
|
|
|
@@ -111,7 +119,8 @@ Types, grouped by theme:
|
|
|
111
119
|
- **Streams** — `Stream`, `Channel`.
|
|
112
120
|
- **Plugins** — `Plugin` (name, `Config`, `apply`, plus the composition
|
|
113
121
|
methods), `PluginSpec` (the author-facing half), `Scope` (`onDispose` /
|
|
114
|
-
`onError`), `Composite` (a composed chain — itself wireable), `
|
|
122
|
+
`onError`), `Composite` (a composed chain — itself wireable), `Engine`
|
|
123
|
+
(a chain sealed by `expose`: final, named, internals erased), `AnyUnit`.
|
|
115
124
|
- **The composition types** — `InOf`, `OutOf`, `ConfigOf`, `NameOf`,
|
|
116
125
|
`WireArgs` (the `use`/`bind` options: `mapping` required when identity
|
|
117
126
|
wiring fails, `option` required when config is non-void, `as` to rename),
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import type { Disposer, Scope } from "./index.ts";
|
|
5
|
+
import { createKernel, definePlugin } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
function makeScope(disposers: Disposer[]): Scope {
|
|
8
|
+
return {
|
|
9
|
+
onDispose: (disposer) => {
|
|
10
|
+
disposers.push(disposer);
|
|
11
|
+
},
|
|
12
|
+
onError: () => {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
void test("engine.apply activates the chain and returns its visible context", async () => {
|
|
17
|
+
const greet = definePlugin<void, string>({
|
|
18
|
+
name: "greet",
|
|
19
|
+
apply: () => "hello",
|
|
20
|
+
});
|
|
21
|
+
const engine = createKernel().use(greet).expose("greeter");
|
|
22
|
+
|
|
23
|
+
const disposers: Disposer[] = [];
|
|
24
|
+
const output = await engine.apply(undefined, makeScope(disposers), undefined);
|
|
25
|
+
assert.deepEqual(output, { greet: "hello" });
|
|
26
|
+
assert.deepEqual(engine.ctx, { greet: "hello" });
|
|
27
|
+
|
|
28
|
+
// Applying the engine registered its teardown with the caller's scope.
|
|
29
|
+
for (const dispose of disposers.splice(0)) await dispose();
|
|
30
|
+
assert.deepEqual(engine.ctx, {});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
void test("engine.apply feeds the external input through the chain", async () => {
|
|
34
|
+
const echo = definePlugin<{ value: string }, string>({
|
|
35
|
+
name: "echo",
|
|
36
|
+
apply: (input) => input.value.toUpperCase(),
|
|
37
|
+
});
|
|
38
|
+
const engine = createKernel()
|
|
39
|
+
.use(echo, { mapping: (ctx) => ctx as unknown as { value: string } })
|
|
40
|
+
.expose("upper");
|
|
41
|
+
|
|
42
|
+
const output = await engine.apply({ value: "hi" } as unknown as void, makeScope([]), undefined);
|
|
43
|
+
assert.deepEqual(output, { echo: "HI" });
|
|
44
|
+
});
|
package/src/kernel.ts
CHANGED
|
@@ -134,6 +134,9 @@ class CompositeRuntime implements RuntimeUnit {
|
|
|
134
134
|
this.onError = options.onError ?? defaultOnError;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** Set by `expose`: composing onto a sealed chain is a runtime error. */
|
|
138
|
+
private exposed: string | undefined;
|
|
139
|
+
|
|
137
140
|
use(unit: RuntimeUnit, options?: WireOptions): this {
|
|
138
141
|
return this.add(unit, options, true);
|
|
139
142
|
}
|
|
@@ -142,7 +145,18 @@ class CompositeRuntime implements RuntimeUnit {
|
|
|
142
145
|
return this.add(unit, options, false);
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
/** Seal the chain and return it as a named engine — the final artifact. */
|
|
149
|
+
expose(name: string): EngineRuntime {
|
|
150
|
+
// Explicit undefined check: "" is a valid name and must still seal.
|
|
151
|
+
if (this.exposed !== undefined)
|
|
152
|
+
throw new Error(`kernel already exposed as engine "${this.exposed}"`);
|
|
153
|
+
this.exposed = name;
|
|
154
|
+
return new EngineRuntime(this, name);
|
|
155
|
+
}
|
|
156
|
+
|
|
145
157
|
private add(unit: RuntimeUnit, options: WireOptions | undefined, visible: boolean): this {
|
|
158
|
+
if (this.exposed !== undefined)
|
|
159
|
+
throw new Error(`cannot compose onto a kernel exposed as engine "${this.exposed}"`);
|
|
146
160
|
const entry: Entry = {
|
|
147
161
|
unit,
|
|
148
162
|
key: options?.as ?? unit.name,
|
|
@@ -207,6 +221,40 @@ class CompositeRuntime implements RuntimeUnit {
|
|
|
207
221
|
}
|
|
208
222
|
}
|
|
209
223
|
|
|
224
|
+
/**
|
|
225
|
+
* The runtime behind `Composite.expose`: a thin, sealed façade over the
|
|
226
|
+
* chain. Lifecycle delegates to the inner composition; as a nested unit it
|
|
227
|
+
* activates exactly like the chain itself — the only difference is the name
|
|
228
|
+
* and the erased type.
|
|
229
|
+
*/
|
|
230
|
+
class EngineRuntime implements RuntimeUnit {
|
|
231
|
+
constructor(
|
|
232
|
+
private readonly inner: CompositeRuntime,
|
|
233
|
+
readonly name: string,
|
|
234
|
+
) {}
|
|
235
|
+
|
|
236
|
+
/** Public contract: apply the engine directly, like any other unit. */
|
|
237
|
+
apply(input: unknown, scope: Scope, config: unknown): Promise<unknown> {
|
|
238
|
+
return this.activate(input, scope, config);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
activate(input: unknown, scope: Scope, _config: unknown): Promise<unknown> {
|
|
242
|
+
return this.inner.activate(input, scope, undefined);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
start(input?: unknown): Promise<void> {
|
|
246
|
+
return this.inner.start(input);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
stop(): Promise<void> {
|
|
250
|
+
return this.inner.stop();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
get ctx(): Record<string, unknown> {
|
|
254
|
+
return this.inner.ctx;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
210
258
|
/**
|
|
211
259
|
* Author a plugin. The spec is just `name`, optional `Config` schema, and
|
|
212
260
|
* `apply`; the returned plugin carries the composition methods (`use`,
|
package/src/plugin.ts
CHANGED
|
@@ -73,12 +73,42 @@ export interface Composite<TIn = void, TVisible = {}, THidden = {}, TName extend
|
|
|
73
73
|
...args: WireArgs<TUnit, InputPart<TIn> & TVisible & THidden, TAs>
|
|
74
74
|
): Composite<TIn, TVisible, THidden & { [K in TAs]: OutOf<TUnit> }, TName>;
|
|
75
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
|
+
|
|
76
85
|
start(...args: StartArgs<TIn>): Promise<void>;
|
|
77
86
|
stop(): Promise<void>;
|
|
78
87
|
readonly ctx: TVisible;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
/**
|
|
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.
|
|
97
|
+
*/
|
|
98
|
+
export interface Engine<TIn = void, TOut = {}, TName extends string = string> {
|
|
99
|
+
readonly name: TName;
|
|
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;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type AnyUnit =
|
|
109
|
+
| Plugin<any, any, any, any>
|
|
110
|
+
| Composite<any, any, any, any>
|
|
111
|
+
| Engine<any, any, any>;
|
|
82
112
|
|
|
83
113
|
/** The kernel is a composition seeded empty: `createKernel().use(...)`. */
|
|
84
114
|
export type Kernel<TVisible = {}, THidden = {}> = Composite<void, TVisible, THidden, "kernel">;
|