@lambdot/websocket 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 +15 -0
- package/README.md +145 -0
- package/package.json +18 -13
- package/src/index.ts +99 -155
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`71e5732`](https://github.com/Embers-of-the-Fire/lambdot/commit/71e57321ad4ec7d1aef3651d104123f8167ec2e7), [`71e5732`](https://github.com/Embers-of-the-Fire/lambdot/commit/71e57321ad4ec7d1aef3651d104123f8167ec2e7)]:
|
|
8
|
+
- @lambdot/core@0.2.0
|
|
9
|
+
|
|
10
|
+
## 0.1.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- [#10](https://github.com/Embers-of-the-Fire/lambdot/pull/10) [`19d37e4`](https://github.com/Embers-of-the-Fire/lambdot/commit/19d37e42c7a5514fb62c8f31c65e1aa01916d355) Thanks [@Embers-of-the-Fire](https://github.com/Embers-of-the-Fire)! - Switch inter-package dependency pins from exact versions to `workspace:*` so workspace members always resolve against local sources during development; pnpm rewrites the protocol to exact versions at pack/publish time.
|
|
15
|
+
- Updated dependencies [[`19d37e4`](https://github.com/Embers-of-the-Fire/lambdot/commit/19d37e42c7a5514fb62c8f31c65e1aa01916d355)]:
|
|
16
|
+
- @lambdot/core@0.1.1
|
|
17
|
+
|
|
3
18
|
## [0.1.0](https://github.com/Embers-of-the-Fire/lambdot/compare/websocket-v0.0.1...websocket-v0.1.0) (2026-08-29)
|
|
4
19
|
|
|
5
20
|
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# @lambdot/websocket
|
|
2
|
+
|
|
3
|
+
The generic websocket transport. It owns the socket lifecycle — connect,
|
|
4
|
+
fan out incoming text frames, close on unload — and nothing else: no message
|
|
5
|
+
shapes, no addresses, no content contract. Everything chat-specific is
|
|
6
|
+
deferred to a `WsSpec` supplied by consumers, which makes this a core
|
|
7
|
+
behavior (like `console`), not a chat platform. Protocol packages
|
|
8
|
+
(`@lambdot/protocol-qq`, …) ride this transport by supplying their own
|
|
9
|
+
specs.
|
|
10
|
+
|
|
11
|
+
The transport is a **service plugin, not a stream**: `wsTransport(name)`
|
|
12
|
+
emits the live `WsConnection` as its namespace value under `name`. The
|
|
13
|
+
`wsInput(name, spec)` / `wsOutput(name, spec)` factories build a platform's
|
|
14
|
+
two halves from the spec, declaring the connection in their input — wiring
|
|
15
|
+
them before the transport is a compile error, because the `mapping`
|
|
16
|
+
parameter is typed as the namespaces visible so far. The socket opens when
|
|
17
|
+
the transport activates and closes when the composition stops.
|
|
18
|
+
|
|
19
|
+
The name is a parameter, so instances multiply: distinct names compose side
|
|
20
|
+
by side (`wsPlatform("wsecho-a", spec)` next to `wsPlatform("wsecho-b", spec)`),
|
|
21
|
+
and several websocket platforms share one kernel, each platform's input and
|
|
22
|
+
output mapping its own transport's connection.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Prefer the bundle. `wsPlatform(name, spec)` builds one websocket platform as
|
|
27
|
+
a triple of plugins — they stay separate so feature plugins can be wired
|
|
28
|
+
between the input (whose message stream is `use`d) and the output (which is
|
|
29
|
+
terminal and always wired last). The transport and output are usually
|
|
30
|
+
`bind`ed, keeping them internal to the chain:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import type { Message, Stream } from "@lambdot/core";
|
|
34
|
+
import { createKernel, definePlugin, mapStream } from "@lambdot/core";
|
|
35
|
+
import { wsPlatform } from "@lambdot/websocket";
|
|
36
|
+
|
|
37
|
+
import { echoSpec, type WsEchoAddress } from "./echo-spec.ts";
|
|
38
|
+
|
|
39
|
+
const reply = definePlugin({
|
|
40
|
+
name: "reply",
|
|
41
|
+
apply(input: { wsecho: Stream<Message<string, WsEchoAddress>> }) {
|
|
42
|
+
return mapStream(input.wsecho, (event) => ({
|
|
43
|
+
address: event.address,
|
|
44
|
+
content: `echo: ${event.payload}`,
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const wsecho = wsPlatform("wsecho", echoSpec);
|
|
50
|
+
|
|
51
|
+
const kernel = createKernel()
|
|
52
|
+
.bind(wsecho.transport, { option: { url } })
|
|
53
|
+
.use(wsecho.input, { mapping: (ctx) => ({ connection: ctx["wsecho/transport"] }) })
|
|
54
|
+
// identity wiring: reply's input keys already match the visible ctx
|
|
55
|
+
.use(reply)
|
|
56
|
+
.bind(wsecho.output, {
|
|
57
|
+
mapping: (ctx) => ({ connection: ctx["wsecho/transport"], commands: ctx.reply }),
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Reach for the individual `wsTransport` / `wsInput` / `wsOutput` factories
|
|
62
|
+
when a kernel hosts several tagged websocket platforms and the triples must
|
|
63
|
+
interleave with other plugins — the factories the bundle wraps stay exported
|
|
64
|
+
for exactly that. Each output filters the shared command stream back down to
|
|
65
|
+
its own platform tag, so `address.platform` routes every reply out the
|
|
66
|
+
socket it arrived on:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const wsechoA = wsPlatform("wsecho-a", echoSpec("a"));
|
|
70
|
+
const wsechoB = wsPlatform("wsecho-b", echoSpec("b"));
|
|
71
|
+
|
|
72
|
+
const kernel = createKernel()
|
|
73
|
+
.bind(wsechoA.transport, { option: { url: urlA } })
|
|
74
|
+
.use(wsechoA.input, { mapping: (ctx) => ({ connection: ctx["wsecho-a/transport"] }) })
|
|
75
|
+
.bind(wsechoB.transport, { option: { url: urlB } })
|
|
76
|
+
.use(wsechoB.input, { mapping: (ctx) => ({ connection: ctx["wsecho-b/transport"] }) })
|
|
77
|
+
.use(reply)
|
|
78
|
+
.bind(wsechoA.output, {
|
|
79
|
+
mapping: (ctx) => ({
|
|
80
|
+
connection: ctx["wsecho-a/transport"],
|
|
81
|
+
commands: filterStream(
|
|
82
|
+
ctx.reply,
|
|
83
|
+
(cmd): cmd is Command<AddressA, string> => cmd.address.platform === "wsecho-a",
|
|
84
|
+
),
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
.bind(wsechoB.output, {/* ...same for "wsecho-b"... */});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A concrete platform supplies one `WsSpec` — platform tag, address shape,
|
|
91
|
+
frame codec — and nothing else:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import type { Address } from "@lambdot/core";
|
|
95
|
+
import type { WsSpec } from "@lambdot/websocket";
|
|
96
|
+
|
|
97
|
+
export type WsEchoAddress = Address<"wsecho">;
|
|
98
|
+
|
|
99
|
+
export const echoSpec: WsSpec<"wsecho", WsEchoAddress, string, string> = {
|
|
100
|
+
platform: "wsecho",
|
|
101
|
+
decode: (data) => ({ payload: data, address: { platform: "wsecho" } }),
|
|
102
|
+
encode: (content) => content,
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`decode` may return `null` to ignore a frame. Adding a second platform
|
|
107
|
+
costs one spec object, ~5 lines; the transport, factories, and feature
|
|
108
|
+
plugins don't change.
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
- `wsPlatform(name, spec)` — bundle a transport and the input/output halves
|
|
113
|
+
built from `spec` into a `WsPlatform` triple (`{ transport, input, output }`).
|
|
114
|
+
The transport is named `${name}/transport`, the input `name`, the output
|
|
115
|
+
`${name}/output`.
|
|
116
|
+
- `wsTransport(name)` — the general half: connection lifecycle only. Config
|
|
117
|
+
is `{ url }` (passed via `option`); emits the live `WsConnection` under
|
|
118
|
+
the name.
|
|
119
|
+
- `wsInput(name, spec)` — subscribes to the connection, decodes each frame,
|
|
120
|
+
and emits a shared `Stream<Message<TPayload, TAddress>>` of the decoded
|
|
121
|
+
messages.
|
|
122
|
+
- `wsOutput(name, spec)` — consumes a `Stream<Command<TAddress, TContent>>`
|
|
123
|
+
and sends each command's encoded content through the connection. Terminal:
|
|
124
|
+
wire it last, after the features whose reply streams it consumes.
|
|
125
|
+
- `WsSpec<TPlatform, TAddress, TPayload, TContent>` — the deferred behavior
|
|
126
|
+
slot: `platform`, plus the `decode` / `encode` frame codec.
|
|
127
|
+
- `WsConnection` — the shared transport service: `url`, `send(data)`,
|
|
128
|
+
`onMessage(listener)` (text frames only; binary frames are dropped).
|
|
129
|
+
- `WsTransportConfig` — `{ readonly url: string }`.
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
- [../../../examples/websocket-bot](../../../examples/websocket-bot) — one platform
|
|
134
|
+
end to end: a raw driver round-trips through server, transport, input,
|
|
135
|
+
reply feature, and output.
|
|
136
|
+
- [../../../examples/dual-websocket-bot](../../../examples/dual-websocket-bot) — two
|
|
137
|
+
tagged platforms sharing one kernel under distinct names, each output
|
|
138
|
+
filtering the shared command stream by `address.platform`.
|
|
139
|
+
|
|
140
|
+
Protocol packages under `packages/protocol/` ride this transport, supplying
|
|
141
|
+
their wire protocol's address type and frame codec as a `WsSpec`.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
Dual-licensed under [Apache-2.0](../../../LICENSE-APACHE) and [MIT](../../../LICENSE-MIT).
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
"name": "@lambdot/websocket",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/Embers-of-the-Fire/lambdot",
|
|
7
|
+
"directory": "packages/core/websocket"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@lambdot/core": "0.2.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
Disposer,
|
|
4
|
-
EventDef,
|
|
5
|
-
FeaturePlugin,
|
|
6
|
-
InputPlugin,
|
|
7
|
-
OutputPlugin,
|
|
8
|
-
} from "@lambdot/core";
|
|
1
|
+
import type { Address, Command, Message, Plugin, Stream } from "@lambdot/core";
|
|
2
|
+
import { channel, definePlugin, message, pumpStream, shareStream } from "@lambdot/core";
|
|
9
3
|
|
|
10
4
|
/**
|
|
11
|
-
* The shared transport service,
|
|
12
|
-
* socket; platform plugins consume it — the transport
|
|
13
|
-
* about platforms, addresses, or
|
|
5
|
+
* The shared transport service, emitted as a plugin's output value. Owns the
|
|
6
|
+
* socket; platform plugins consume it through their input — the transport
|
|
7
|
+
* itself knows nothing about platforms, addresses, or message shapes.
|
|
14
8
|
*/
|
|
15
9
|
export interface WsConnection {
|
|
16
10
|
readonly url: string;
|
|
17
11
|
/** Send a text frame. */
|
|
18
12
|
send(data: string): void;
|
|
19
|
-
/** Subscribe to incoming text frames.
|
|
20
|
-
onMessage(listener: (data: string) => void):
|
|
13
|
+
/** Subscribe to incoming text frames. The listener remains active for the connection lifetime. */
|
|
14
|
+
onMessage(listener: (data: string) => void): void;
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
export interface WsTransportConfig {
|
|
@@ -25,28 +19,16 @@ export interface WsTransportConfig {
|
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
/**
|
|
28
|
-
* The
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* side by side (`WsCapability<"ws-discord"> & WsCapability<"ws-qq">`), so
|
|
32
|
-
* several websocket platforms can share one kernel — the kernel rejects a
|
|
33
|
-
* duplicate capability name at runtime, and the type fold makes each
|
|
34
|
-
* consumer read back its own connection with no casts.
|
|
22
|
+
* The general half: connection lifecycle only. Emits the live connection as
|
|
23
|
+
* its output value; platform plugins declare it as their input. The socket
|
|
24
|
+
* opens at activation and closes when the composition stops.
|
|
35
25
|
*/
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* available via `inject: [capability]`.
|
|
43
|
-
*/
|
|
44
|
-
export function wsTransport<TCap extends string>(
|
|
45
|
-
capability: TCap,
|
|
46
|
-
): FeaturePlugin<{}, {}, undefined, WsTransportConfig, `ws-transport:${TCap}`, WsCapability<TCap>> {
|
|
47
|
-
return {
|
|
48
|
-
name: `ws-transport:${capability}`,
|
|
49
|
-
async apply(ctx, config) {
|
|
26
|
+
export function wsTransport<const TName extends string>(
|
|
27
|
+
name: TName,
|
|
28
|
+
): Plugin<void, WsConnection, WsTransportConfig, TName> {
|
|
29
|
+
return definePlugin({
|
|
30
|
+
name,
|
|
31
|
+
async apply(_input, scope, config) {
|
|
50
32
|
const socket = new WebSocket(config.url);
|
|
51
33
|
await new Promise<void>((resolve, reject) => {
|
|
52
34
|
socket.addEventListener("open", () => resolve(), { once: true });
|
|
@@ -58,199 +40,161 @@ export function wsTransport<TCap extends string>(
|
|
|
58
40
|
},
|
|
59
41
|
);
|
|
60
42
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
socket.addEventListener("message", (event) => {
|
|
64
|
-
if (typeof event.data !== "string") return;
|
|
65
|
-
for (const listener of listeners) listener(event.data);
|
|
43
|
+
scope.onDispose(() => {
|
|
44
|
+
socket.close();
|
|
66
45
|
});
|
|
67
46
|
|
|
68
|
-
|
|
47
|
+
return {
|
|
69
48
|
url: config.url,
|
|
70
49
|
send: (data) => socket.send(data),
|
|
71
50
|
onMessage(listener) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
51
|
+
socket.addEventListener("message", (event) => {
|
|
52
|
+
if (typeof event.data === "string") listener(event.data);
|
|
53
|
+
});
|
|
76
54
|
},
|
|
77
55
|
};
|
|
78
|
-
|
|
79
|
-
// The kernel's `provide` keeps its value parameter behind a
|
|
80
|
-
// conditional type that stays deferred for a generic capability
|
|
81
|
-
// name; `WsCapability<TCap>` already ties this name to
|
|
82
|
-
// `WsConnection`, so pin the call down here.
|
|
83
|
-
const unprovide = (ctx.provide as (name: TCap, value: WsConnection) => Disposer).call(
|
|
84
|
-
ctx,
|
|
85
|
-
capability,
|
|
86
|
-
connection,
|
|
87
|
-
);
|
|
88
|
-
return () => {
|
|
89
|
-
socket.close();
|
|
90
|
-
void unprovide();
|
|
91
|
-
};
|
|
92
56
|
},
|
|
93
|
-
};
|
|
57
|
+
});
|
|
94
58
|
}
|
|
95
59
|
|
|
96
60
|
/**
|
|
97
61
|
* The deferred behavior slot: everything the generic transport cannot know —
|
|
98
|
-
* the platform tag, the
|
|
99
|
-
* (discord, qq, ...) supplies one of these to the factories below.
|
|
62
|
+
* the platform tag, the address shape, and the frame codec. A concrete
|
|
63
|
+
* platform (discord, qq, ...) supplies one of these to the factories below.
|
|
100
64
|
*/
|
|
101
65
|
export interface WsSpec<
|
|
102
66
|
TPlatform extends string,
|
|
103
67
|
TAddress extends Address<TPlatform>,
|
|
104
68
|
TPayload,
|
|
105
69
|
TContent,
|
|
106
|
-
TKind extends string,
|
|
107
70
|
> {
|
|
108
71
|
readonly platform: TPlatform;
|
|
109
|
-
|
|
110
|
-
/** Decode an incoming frame into an event, or null to ignore the frame. */
|
|
72
|
+
/** Decode an incoming frame into a message, or null to ignore the frame. */
|
|
111
73
|
decode(data: string): { payload: TPayload; address: TAddress } | null;
|
|
112
74
|
/** Encode outgoing content into a text frame. */
|
|
113
75
|
encode(content: TContent, address: TAddress): string;
|
|
114
76
|
}
|
|
115
77
|
|
|
116
78
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* to the concrete instantiation.
|
|
79
|
+
* The input half of a websocket platform: consumes the connection and emits
|
|
80
|
+
* the stream of decoded messages. Wire the connection with a mapping from
|
|
81
|
+
* the transport's namespace.
|
|
121
82
|
*/
|
|
122
83
|
export function wsInput<
|
|
123
|
-
|
|
84
|
+
const TName extends string,
|
|
124
85
|
TPlatform extends string,
|
|
125
86
|
TAddress extends Address<TPlatform>,
|
|
126
87
|
TPayload,
|
|
127
88
|
TContent,
|
|
128
|
-
TKind extends string,
|
|
129
89
|
>(
|
|
130
|
-
|
|
131
|
-
spec: WsSpec<TPlatform, TAddress, TPayload, TContent
|
|
132
|
-
):
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
> {
|
|
139
|
-
return {
|
|
140
|
-
role: "input",
|
|
141
|
-
name: `${spec.platform}-input`,
|
|
142
|
-
inject: [capability],
|
|
143
|
-
apply(ctx) {
|
|
144
|
-
return ctx[capability].onMessage((data) => {
|
|
90
|
+
name: TName,
|
|
91
|
+
spec: WsSpec<TPlatform, TAddress, TPayload, TContent>,
|
|
92
|
+
): Plugin<{ connection: WsConnection }, Stream<Message<TPayload, TAddress>>, void, TName> {
|
|
93
|
+
return definePlugin({
|
|
94
|
+
name,
|
|
95
|
+
apply(input, scope) {
|
|
96
|
+
const messages = channel<Message<TPayload, TAddress>>();
|
|
97
|
+
input.connection.onMessage((data) => {
|
|
145
98
|
const decoded = spec.decode(data);
|
|
146
|
-
if (decoded)
|
|
99
|
+
if (decoded) messages.push(message(decoded.payload, decoded.address));
|
|
147
100
|
});
|
|
101
|
+
scope.onDispose(() => {
|
|
102
|
+
messages.close();
|
|
103
|
+
});
|
|
104
|
+
// Shared: several consumers may subscribe to the message stream.
|
|
105
|
+
return shareStream(messages.stream);
|
|
148
106
|
},
|
|
149
|
-
};
|
|
107
|
+
});
|
|
150
108
|
}
|
|
151
109
|
|
|
152
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* The output half of a websocket platform: consumes a command stream and
|
|
112
|
+
* sends each command's encoded content through the connection. Terminal —
|
|
113
|
+
* wire it last, after the features whose reply streams it consumes.
|
|
114
|
+
*/
|
|
153
115
|
export function wsOutput<
|
|
154
|
-
|
|
116
|
+
const TName extends string,
|
|
155
117
|
TPlatform extends string,
|
|
156
118
|
TAddress extends Address<TPlatform>,
|
|
157
119
|
TPayload,
|
|
158
120
|
TContent,
|
|
159
|
-
TKind extends string,
|
|
160
121
|
>(
|
|
161
|
-
|
|
162
|
-
spec: WsSpec<TPlatform, TAddress, TPayload, TContent
|
|
163
|
-
):
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
TContent,
|
|
122
|
+
name: TName,
|
|
123
|
+
spec: WsSpec<TPlatform, TAddress, TPayload, TContent>,
|
|
124
|
+
): Plugin<
|
|
125
|
+
{ connection: WsConnection; commands: Stream<Command<TAddress, TContent>> },
|
|
126
|
+
void,
|
|
167
127
|
void,
|
|
168
|
-
|
|
169
|
-
{},
|
|
170
|
-
WsCapability<TCap>
|
|
128
|
+
TName
|
|
171
129
|
> {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
connection = ctx[capability];
|
|
184
|
-
return () => {
|
|
185
|
-
connection = undefined;
|
|
186
|
-
};
|
|
130
|
+
return definePlugin({
|
|
131
|
+
name,
|
|
132
|
+
apply(input, scope) {
|
|
133
|
+
const { connection } = input;
|
|
134
|
+
scope.onDispose(
|
|
135
|
+
pumpStream(
|
|
136
|
+
input.commands,
|
|
137
|
+
(cmd) => connection.send(spec.encode(cmd.content, cmd.address)),
|
|
138
|
+
(error) => scope.onError(error),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
187
141
|
},
|
|
188
|
-
};
|
|
142
|
+
});
|
|
189
143
|
}
|
|
190
144
|
|
|
191
145
|
/**
|
|
192
|
-
* One websocket platform, bundled
|
|
193
|
-
* `
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
|
|
146
|
+
* One websocket platform, bundled as three leaves. The transport and output
|
|
147
|
+
* are usually `bind`ed (internal wiring); the input's message stream is
|
|
148
|
+
* `use`d (exposed to features). The output is terminal, so it is always
|
|
149
|
+
* wired last:
|
|
150
|
+
|
|
197
151
|
* ```ts
|
|
198
|
-
* const
|
|
152
|
+
* const wsecho = wsPlatform("wsecho", echoSpec);
|
|
199
153
|
* createKernel()
|
|
200
|
-
* .
|
|
201
|
-
* .use(
|
|
202
|
-
* .use(
|
|
154
|
+
* .bind(wsecho.transport, { option: { url } })
|
|
155
|
+
* .use(wsecho.input, { mapping: (ctx) => ({ connection: ctx["wsecho/transport"] }) })
|
|
156
|
+
* .use(reply)
|
|
157
|
+
* .bind(wsecho.output, {
|
|
158
|
+
* mapping: (ctx) => ({ connection: ctx["wsecho/transport"], commands: ctx.reply }),
|
|
159
|
+
* });
|
|
203
160
|
* ```
|
|
204
161
|
*/
|
|
205
162
|
export interface WsPlatform<
|
|
206
|
-
|
|
163
|
+
TName extends string,
|
|
207
164
|
TPlatform extends string,
|
|
208
165
|
TAddress extends Address<TPlatform>,
|
|
209
166
|
TPayload,
|
|
210
167
|
TContent,
|
|
211
|
-
TKind extends string,
|
|
212
168
|
> {
|
|
213
|
-
readonly transport:
|
|
214
|
-
|
|
215
|
-
{},
|
|
216
|
-
|
|
217
|
-
WsTransportConfig,
|
|
218
|
-
`ws-transport:${TCap}`,
|
|
219
|
-
WsCapability<TCap>
|
|
220
|
-
>;
|
|
221
|
-
readonly input: InputPlugin<
|
|
222
|
-
{ [K in TKind]: EventDef<TPayload, TAddress> },
|
|
169
|
+
readonly transport: Plugin<void, WsConnection, WsTransportConfig, `${TName}/transport`>;
|
|
170
|
+
readonly input: Plugin<
|
|
171
|
+
{ connection: WsConnection },
|
|
172
|
+
Stream<Message<TPayload, TAddress>>,
|
|
223
173
|
void,
|
|
224
|
-
|
|
225
|
-
{},
|
|
226
|
-
WsCapability<TCap>
|
|
174
|
+
TName
|
|
227
175
|
>;
|
|
228
|
-
readonly output:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
TContent,
|
|
176
|
+
readonly output: Plugin<
|
|
177
|
+
{ connection: WsConnection; commands: Stream<Command<TAddress, TContent>> },
|
|
178
|
+
void,
|
|
232
179
|
void,
|
|
233
|
-
`${
|
|
234
|
-
{},
|
|
235
|
-
WsCapability<TCap>
|
|
180
|
+
`${TName}/output`
|
|
236
181
|
>;
|
|
237
182
|
}
|
|
238
183
|
|
|
239
|
-
/** Build a whole websocket platform (transport + input + output) from a
|
|
184
|
+
/** Build a whole websocket platform (transport + input + output) from a name and a spec. */
|
|
240
185
|
export function wsPlatform<
|
|
241
|
-
|
|
186
|
+
const TName extends string,
|
|
242
187
|
TPlatform extends string,
|
|
243
188
|
TAddress extends Address<TPlatform>,
|
|
244
189
|
TPayload,
|
|
245
190
|
TContent,
|
|
246
|
-
TKind extends string,
|
|
247
191
|
>(
|
|
248
|
-
|
|
249
|
-
spec: WsSpec<TPlatform, TAddress, TPayload, TContent
|
|
250
|
-
): WsPlatform<
|
|
192
|
+
name: TName,
|
|
193
|
+
spec: WsSpec<TPlatform, TAddress, TPayload, TContent>,
|
|
194
|
+
): WsPlatform<TName, TPlatform, TAddress, TPayload, TContent> {
|
|
251
195
|
return {
|
|
252
|
-
transport: wsTransport(
|
|
253
|
-
input: wsInput(
|
|
254
|
-
output: wsOutput(
|
|
196
|
+
transport: wsTransport(`${name}/transport`),
|
|
197
|
+
input: wsInput(name, spec),
|
|
198
|
+
output: wsOutput(`${name}/output`, spec),
|
|
255
199
|
};
|
|
256
200
|
}
|