@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/kernel.ts
CHANGED
|
@@ -1,306 +1,275 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type {
|
|
3
|
-
import { collectEffect, type Disposer } from "./effect.ts";
|
|
4
|
-
import { EventBus, INGRESS, type BotEvent, type OnOptions } from "./events.ts";
|
|
5
|
-
import type { EventMap } from "./events.ts";
|
|
6
|
-
import { Fiber } from "./fiber.ts";
|
|
7
|
-
import type {
|
|
8
|
-
AnyPlugin,
|
|
9
|
-
CapsOf,
|
|
10
|
-
ConfigOf,
|
|
11
|
-
EventsOf,
|
|
12
|
-
OutputsOf,
|
|
13
|
-
Spread,
|
|
14
|
-
StateOf,
|
|
15
|
-
Validate,
|
|
16
|
-
} from "./plugin.ts";
|
|
1
|
+
import { runDisposers, type Disposer } from "./effect.ts";
|
|
2
|
+
import type { Composite, Plugin, PluginSpec, Scope } from "./plugin.ts";
|
|
17
3
|
import { validateConfig } from "./schema.ts";
|
|
18
|
-
import { createStateAccessor, type StateBackend } from "./state.ts";
|
|
19
4
|
|
|
20
5
|
export interface KernelOptions {
|
|
21
|
-
/** Sink for errors
|
|
6
|
+
/** Sink for errors reported through `scope.onError` and background activations. */
|
|
22
7
|
onError?: (error: unknown) => void;
|
|
23
8
|
}
|
|
24
9
|
|
|
10
|
+
/** Runtime view of anything wireable: a leaf plugin or a nested composition. */
|
|
11
|
+
interface RuntimeUnit {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
activate(input: unknown, scope: Scope, config: unknown): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Entry {
|
|
17
|
+
readonly unit: RuntimeUnit;
|
|
18
|
+
/** The namespace key the unit's output is stored under (`as` ?? name). */
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly visible: boolean;
|
|
21
|
+
readonly mapping: ((ctx: Record<string, unknown>) => unknown) | undefined;
|
|
22
|
+
readonly config: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultOnError = (error: unknown): void => console.error("[lambdot]", error);
|
|
26
|
+
|
|
27
|
+
function makeScope(disposers: Disposer[], onError: (error: unknown) => void): Scope {
|
|
28
|
+
return {
|
|
29
|
+
onDispose: (disposer) => {
|
|
30
|
+
disposers.push(disposer);
|
|
31
|
+
},
|
|
32
|
+
onError,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
37
|
+
return typeof value === "object" && value !== null;
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
41
|
+
* A leaf plugin at runtime. The composition methods seed an artifact chain
|
|
42
|
+
* with the plugin itself as the first entry, so a lone plugin is runnable.
|
|
28
43
|
*/
|
|
29
|
-
class
|
|
30
|
-
|
|
31
|
-
private readonly outputs = new Map<
|
|
32
|
-
string,
|
|
33
|
-
{ send(to: never, content: never): void | Promise<void> }
|
|
34
|
-
>();
|
|
35
|
-
private readonly provided = new Map<string, unknown>();
|
|
36
|
-
/** Events are processed sequentially, in ingestion order. */
|
|
37
|
-
private queue: Promise<void> = Promise.resolve();
|
|
38
|
-
onProvideChange: (name: string, available: boolean) => void = () => {};
|
|
39
|
-
|
|
40
|
-
constructor(private readonly onError: (error: unknown) => void) {
|
|
41
|
-
this.bus = new EventBus(onError);
|
|
42
|
-
}
|
|
44
|
+
class PluginRuntime implements RuntimeUnit {
|
|
45
|
+
private artifact: CompositeRuntime | undefined;
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
constructor(private readonly spec: PluginSpec<any, any, any, string>) {
|
|
48
|
+
this.name = spec.name;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
this.bus.emit(kind, event);
|
|
50
|
-
}
|
|
51
|
+
readonly name: string;
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
return this.
|
|
53
|
+
get Config(): PluginSpec<any, any, any, string>["Config"] {
|
|
54
|
+
return this.spec.Config;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
return this.
|
|
57
|
+
apply(input: unknown, scope: Scope, config: unknown): Promise<unknown> {
|
|
58
|
+
return this.activate(input, scope, config);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return this.bus.waterfall(kind, event, inner);
|
|
61
|
+
async activate(input: unknown, scope: Scope, config: unknown): Promise<unknown> {
|
|
62
|
+
const validated = this.spec.Config
|
|
63
|
+
? await validateConfig(this.spec.name, this.spec.Config, config)
|
|
64
|
+
: config;
|
|
65
|
+
return await this.spec.apply(input, scope, validated);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!output) throw new Error(`no output registered for platform "${to.platform}"`);
|
|
71
|
-
await output.send(to as never, content as never);
|
|
68
|
+
use(unit: RuntimeUnit, options?: WireOptions): CompositeRuntime {
|
|
69
|
+
return this.asArtifact().use(unit, options);
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
output: { send(to: never, content: never): void | Promise<void> },
|
|
77
|
-
): Disposer {
|
|
78
|
-
if (this.outputs.has(platform))
|
|
79
|
-
throw new Error(`duplicate output for platform "${platform}"`);
|
|
80
|
-
this.outputs.set(platform, output);
|
|
81
|
-
return () => {
|
|
82
|
-
this.outputs.delete(platform);
|
|
83
|
-
};
|
|
72
|
+
bind(unit: RuntimeUnit, options?: WireOptions): CompositeRuntime {
|
|
73
|
+
return this.asArtifact().bind(unit, options);
|
|
84
74
|
}
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// State has no dedicated runtime slot: a state plugin is an
|
|
89
|
-
// ordinary feature plugin that provides its backend as the
|
|
90
|
-
// (runtime-gated) "state" capability.
|
|
91
|
-
const backend = this.provided.get("state") as StateBackend | undefined;
|
|
92
|
-
if (!backend)
|
|
93
|
-
throw new Error(
|
|
94
|
-
'no state backend active — register a state plugin and add `inject: ["state"]`',
|
|
95
|
-
);
|
|
96
|
-
return createStateAccessor(backend, plugin);
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
provide(name: string, value?: unknown): Disposer {
|
|
101
|
-
if (this.provided.has(name)) throw new Error(`capability "${name}" is already provided`);
|
|
102
|
-
this.provided.set(name, value);
|
|
103
|
-
const defined = value !== undefined && !(name in this);
|
|
104
|
-
if (defined) {
|
|
105
|
-
Object.defineProperty(this, name, { value, configurable: true });
|
|
106
|
-
}
|
|
107
|
-
this.onProvideChange(name, true);
|
|
108
|
-
return () => {
|
|
109
|
-
this.provided.delete(name);
|
|
110
|
-
if (defined) {
|
|
111
|
-
delete (this as Record<string, unknown>)[name];
|
|
112
|
-
}
|
|
113
|
-
this.onProvideChange(name, false);
|
|
114
|
-
};
|
|
76
|
+
start(input?: unknown): Promise<void> {
|
|
77
|
+
return this.asArtifact().start(input);
|
|
115
78
|
}
|
|
116
79
|
|
|
117
|
-
|
|
118
|
-
|
|
80
|
+
async stop(): Promise<void> {
|
|
81
|
+
await this.artifact?.stop();
|
|
119
82
|
}
|
|
120
83
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const run = this.queue.then(() => this.process(event));
|
|
124
|
-
// A failing event rejects its own caller but never jams the queue.
|
|
125
|
-
this.queue = run.catch(this.onError);
|
|
126
|
-
return run;
|
|
84
|
+
get ctx(): Record<string, unknown> {
|
|
85
|
+
return this.asArtifact().ctx;
|
|
127
86
|
}
|
|
128
87
|
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
88
|
+
private asArtifact(): CompositeRuntime {
|
|
89
|
+
if (!this.artifact) {
|
|
90
|
+
this.artifact = new CompositeRuntime(
|
|
91
|
+
[
|
|
92
|
+
{
|
|
93
|
+
unit: this,
|
|
94
|
+
key: this.spec.name,
|
|
95
|
+
visible: true,
|
|
96
|
+
mapping: undefined,
|
|
97
|
+
config: undefined,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
{},
|
|
101
|
+
this.spec.name,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return this.artifact;
|
|
133
105
|
}
|
|
134
106
|
}
|
|
135
107
|
|
|
136
|
-
interface
|
|
137
|
-
readonly
|
|
138
|
-
readonly
|
|
139
|
-
readonly
|
|
108
|
+
interface WireOptions {
|
|
109
|
+
readonly mapping?: (ctx: Record<string, unknown>) => unknown;
|
|
110
|
+
readonly option?: unknown;
|
|
111
|
+
readonly as?: string;
|
|
140
112
|
}
|
|
141
113
|
|
|
142
114
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
115
|
+
* A composition at runtime. Inert while built: `start` activates the entries
|
|
116
|
+
* in composition order — resolve the mapping (or identity-wire the visible
|
|
117
|
+
* context), activate the unit, stash its output under its namespace key —
|
|
118
|
+
* and `stop` disposes in reverse. Activation order is definition order;
|
|
119
|
+
* ordering mistakes are compile errors in the mappings, not runtime states.
|
|
147
120
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
> {
|
|
154
|
-
/** The typed context. This is the fold of everything registered so far. */
|
|
155
|
-
readonly ctx: ContextView<TEvents, TOutputs, TState, TCaps> & TCaps;
|
|
156
|
-
|
|
157
|
-
private readonly runtime: RuntimeContext;
|
|
158
|
-
private readonly entries: FiberEntry[] = [];
|
|
159
|
-
private readonly onError: (error: unknown) => void;
|
|
160
|
-
private readonly inflightActivations = new Set<Promise<void>>();
|
|
121
|
+
class CompositeRuntime implements RuntimeUnit {
|
|
122
|
+
readonly ctx: Record<string, unknown> = {};
|
|
123
|
+
private readonly hidden: Record<string, unknown> = {};
|
|
124
|
+
private readonly activated: { entry: Entry; disposers: Disposer[] }[] = [];
|
|
125
|
+
private inputRecord: Record<string, unknown> = {};
|
|
161
126
|
private started = false;
|
|
127
|
+
private readonly onError: (error: unknown) => void;
|
|
162
128
|
|
|
163
|
-
constructor(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
129
|
+
constructor(
|
|
130
|
+
private readonly entries: Entry[],
|
|
131
|
+
options: KernelOptions,
|
|
132
|
+
readonly name: string,
|
|
133
|
+
) {
|
|
134
|
+
this.onError = options.onError ?? defaultOnError;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Set by `expose`: composing onto a sealed chain is a runtime error. */
|
|
138
|
+
private exposed: string | undefined;
|
|
139
|
+
|
|
140
|
+
use(unit: RuntimeUnit, options?: WireOptions): this {
|
|
141
|
+
return this.add(unit, options, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
bind(unit: RuntimeUnit, options?: WireOptions): this {
|
|
145
|
+
return this.add(unit, options, false);
|
|
146
|
+
}
|
|
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);
|
|
171
155
|
}
|
|
172
156
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
TEvents & EventsOf<TPlugin>,
|
|
183
|
-
TOutputs & OutputsOf<TPlugin>,
|
|
184
|
-
TCaps & CapsOf<TPlugin>,
|
|
185
|
-
TState & StateOf<TPlugin>
|
|
186
|
-
> {
|
|
187
|
-
const entry: FiberEntry = {
|
|
188
|
-
plugin,
|
|
189
|
-
config: config[0],
|
|
190
|
-
fiber: new Fiber(plugin, config[0]),
|
|
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}"`);
|
|
160
|
+
const entry: Entry = {
|
|
161
|
+
unit,
|
|
162
|
+
key: options?.as ?? unit.name,
|
|
163
|
+
visible,
|
|
164
|
+
mapping: options?.mapping,
|
|
165
|
+
config: options?.option,
|
|
191
166
|
};
|
|
192
167
|
this.entries.push(entry);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
168
|
+
// Composing onto a running artifact activates in the background —
|
|
169
|
+
// same fire-and-report semantics as composition before `start`.
|
|
170
|
+
if (this.started) void this.activateEntry(entry).catch(this.onError);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** RuntimeUnit: run as a nested unit, torn down with the parent. */
|
|
175
|
+
async activate(input: unknown, scope: Scope, _config: unknown): Promise<unknown> {
|
|
176
|
+
scope.onDispose(() => this.stop());
|
|
177
|
+
await this.start(input);
|
|
178
|
+
return this.ctx;
|
|
200
179
|
}
|
|
201
180
|
|
|
202
|
-
|
|
203
|
-
async start(): Promise<void> {
|
|
181
|
+
async start(input?: unknown): Promise<void> {
|
|
204
182
|
if (this.started) return;
|
|
205
183
|
this.started = true;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
184
|
+
this.inputRecord = isRecord(input) ? input : {};
|
|
185
|
+
try {
|
|
186
|
+
for (const entry of this.entries) {
|
|
187
|
+
if (this.activated.some((done) => done.entry === entry)) continue;
|
|
188
|
+
await this.activateEntry(entry);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
await this.stop();
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
212
194
|
}
|
|
213
195
|
|
|
214
|
-
/** Dispose every active fiber in reverse registration order. */
|
|
215
196
|
async stop(): Promise<void> {
|
|
216
197
|
this.started = false;
|
|
217
|
-
for (const entry of
|
|
218
|
-
await
|
|
198
|
+
for (const { entry, disposers } of this.activated.splice(0).reverse()) {
|
|
199
|
+
await runDisposers(disposers, this.onError);
|
|
200
|
+
if (entry.visible) delete this.ctx[entry.key];
|
|
201
|
+
else delete this.hidden[entry.key];
|
|
219
202
|
}
|
|
220
203
|
}
|
|
221
204
|
|
|
222
|
-
private
|
|
223
|
-
|
|
205
|
+
private async activateEntry(entry: Entry): Promise<void> {
|
|
206
|
+
if (entry.key in this.ctx || entry.key in this.hidden)
|
|
207
|
+
throw new Error(`duplicate namespace "${entry.key}"`);
|
|
208
|
+
const disposers: Disposer[] = [];
|
|
209
|
+
// Identity wiring hands over the whole visible context; the unit's
|
|
210
|
+
// declared input type narrows the view at compile time.
|
|
211
|
+
const wireCtx = { ...this.inputRecord, ...this.hidden, ...this.ctx };
|
|
212
|
+
const input = entry.mapping ? entry.mapping(wireCtx) : wireCtx;
|
|
213
|
+
const output = await entry.unit.activate(
|
|
214
|
+
input,
|
|
215
|
+
makeScope(disposers, this.onError),
|
|
216
|
+
entry.config,
|
|
217
|
+
);
|
|
218
|
+
if (entry.visible) this.ctx[entry.key] = output;
|
|
219
|
+
else this.hidden[entry.key] = output;
|
|
220
|
+
this.activated.push({ entry, disposers });
|
|
224
221
|
}
|
|
222
|
+
}
|
|
225
223
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// Nested passes (triggered by `provide` during activation) may still
|
|
238
|
-
// have activations in flight; wait for quiescence so `start()` and
|
|
239
|
-
// `stop()` only observe settled fibers.
|
|
240
|
-
while (this.inflightActivations.size > 0) {
|
|
241
|
-
await Promise.allSettled(this.inflightActivations);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
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
|
+
) {}
|
|
244
235
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
entry.fiber.state = "activating";
|
|
249
|
-
const activation = this.completeActivation(entry);
|
|
250
|
-
this.inflightActivations.add(activation);
|
|
251
|
-
const settled = () => this.inflightActivations.delete(activation);
|
|
252
|
-
activation.then(settled, settled);
|
|
253
|
-
return activation;
|
|
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);
|
|
254
239
|
}
|
|
255
240
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
if (plugin.Config) config = await validateConfig(plugin.name, plugin.Config, config);
|
|
261
|
-
} catch (error) {
|
|
262
|
-
fiber.state = "pending";
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
// The kernel may have stopped, or a required capability may have been
|
|
266
|
-
// withdrawn, while we awaited; mid-activation fibers are invisible to
|
|
267
|
-
// `deactivateDependents` and `stop`.
|
|
268
|
-
if (!this.started || !this.eligible(plugin)) {
|
|
269
|
-
fiber.state = "pending";
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Role-specific registration happens before `apply` so plugin code
|
|
274
|
-
// can rely on its own capability being live.
|
|
275
|
-
if (plugin.role === "output") {
|
|
276
|
-
fiber.state = "active";
|
|
277
|
-
fiber.addDisposer(this.runtime.registerOutput(plugin.platform, plugin));
|
|
278
|
-
} else {
|
|
279
|
-
fiber.state = "active";
|
|
280
|
-
}
|
|
241
|
+
activate(input: unknown, scope: Scope, _config: unknown): Promise<unknown> {
|
|
242
|
+
return this.inner.activate(input, scope, undefined);
|
|
243
|
+
}
|
|
281
244
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
286
|
-
}
|
|
245
|
+
start(input?: unknown): Promise<void> {
|
|
246
|
+
return this.inner.start(input);
|
|
247
|
+
}
|
|
287
248
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
fiber.addDisposer(this.runtime.provide(name));
|
|
291
|
-
}
|
|
249
|
+
stop(): Promise<void> {
|
|
250
|
+
return this.inner.stop();
|
|
292
251
|
}
|
|
293
252
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (entry.fiber.state !== "active") continue;
|
|
297
|
-
if (!(entry.plugin.inject ?? []).includes(capability)) continue;
|
|
298
|
-
await entry.fiber.dispose("pending", this.onError);
|
|
299
|
-
}
|
|
253
|
+
get ctx(): Record<string, unknown> {
|
|
254
|
+
return this.inner.ctx;
|
|
300
255
|
}
|
|
301
256
|
}
|
|
302
257
|
|
|
303
|
-
/**
|
|
304
|
-
|
|
305
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Author a plugin. The spec is just `name`, optional `Config` schema, and
|
|
260
|
+
* `apply`; the returned plugin carries the composition methods (`use`,
|
|
261
|
+
* `bind`, `start`, `stop`, `ctx`).
|
|
262
|
+
*/
|
|
263
|
+
export function definePlugin<
|
|
264
|
+
TIn = void,
|
|
265
|
+
TOut = void,
|
|
266
|
+
TConfig = void,
|
|
267
|
+
TName extends string = string,
|
|
268
|
+
>(spec: PluginSpec<TIn, TOut, TConfig, TName>): Plugin<TIn, TOut, TConfig, TName> {
|
|
269
|
+
return new PluginRuntime(spec) as never;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Create an empty composition. Wire plugins in with `.use(...)` / `.bind(...)`. */
|
|
273
|
+
export function createKernel(options?: KernelOptions): Composite<void, {}, {}, "kernel"> {
|
|
274
|
+
return new CompositeRuntime([], options ?? {}, "kernel") as never;
|
|
306
275
|
}
|
package/src/message.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The envelope flowing through streams: a payload plus its return address.
|
|
3
|
+
* The core never inspects the address beyond the `platform` routing tag.
|
|
4
|
+
*/
|
|
5
|
+
export interface Address<TPlatform extends string = string> {
|
|
6
|
+
readonly platform: TPlatform;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One inbound event. `payload` and `address` are platform-defined; `id` and
|
|
11
|
+
* `at` are minted by the producing input (see {@link message}).
|
|
12
|
+
*/
|
|
13
|
+
export interface Message<TPayload = unknown, TAddress = unknown> {
|
|
14
|
+
readonly payload: TPayload;
|
|
15
|
+
readonly address: TAddress;
|
|
16
|
+
/** Unique id for dedup and tracing. */
|
|
17
|
+
readonly id: string;
|
|
18
|
+
/** Unix epoch milliseconds. */
|
|
19
|
+
readonly at: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Mint a message envelope with a fresh id and timestamp. */
|
|
23
|
+
export function message<TPayload, TAddress>(
|
|
24
|
+
payload: TPayload,
|
|
25
|
+
address: TAddress,
|
|
26
|
+
): Message<TPayload, TAddress> {
|
|
27
|
+
return { payload, address, id: crypto.randomUUID(), at: Date.now() };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One outbound reply: content addressed back through the platform that owns
|
|
32
|
+
* the address. Features emit streams of these; output plugins consume them.
|
|
33
|
+
*/
|
|
34
|
+
export interface Command<TAddress = unknown, TContent = unknown> {
|
|
35
|
+
readonly address: TAddress;
|
|
36
|
+
readonly content: TContent;
|
|
37
|
+
}
|