@fncts/node 0.0.33 → 0.0.35
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/_cjs/http/IncomingMessage.cjs +59 -0
- package/_cjs/http/IncomingMessage.cjs.map +1 -0
- package/_cjs/http/Server.cjs +205 -0
- package/_cjs/http/Server.cjs.map +1 -0
- package/_cjs/http/ServerRequest.cjs +43 -0
- package/_cjs/http/ServerRequest.cjs.map +1 -0
- package/_cjs/stream/api.cjs +191 -52
- package/_cjs/stream/api.cjs.map +1 -1
- package/_mjs/http/IncomingMessage.mjs +50 -0
- package/_mjs/http/IncomingMessage.mjs.map +1 -0
- package/_mjs/http/Server.mjs +194 -0
- package/_mjs/http/Server.mjs.map +1 -0
- package/_mjs/http/ServerRequest.mjs +34 -0
- package/_mjs/http/ServerRequest.mjs.map +1 -0
- package/_mjs/stream/api.mjs +187 -50
- package/_mjs/stream/api.mjs.map +1 -1
- package/_src/http/IncomingMessage.ts +60 -0
- package/_src/http/Server.ts +282 -0
- package/_src/http/ServerRequest.ts +54 -0
- package/_src/stream/api.ts +247 -50
- package/http/IncomingMessage.d.ts +23 -0
- package/http/Server.d.ts +25 -0
- package/http/ServerRequest.d.ts +29 -0
- package/package.json +3 -2
- package/stream/api.d.ts +23 -8
@@ -0,0 +1,282 @@
|
|
1
|
+
import type { FormData } from "@fncts/http/Body";
|
2
|
+
import type { HttpApp } from "@fncts/http/HttpApp";
|
3
|
+
import type { Middleware } from "@fncts/http/Middleware";
|
4
|
+
import type { ServerResponse } from "@fncts/http/ServerResponse";
|
5
|
+
import type * as Net from "node:net";
|
6
|
+
import type { Duplex } from "node:stream";
|
7
|
+
|
8
|
+
import { BodyTag } from "@fncts/http/Body";
|
9
|
+
import { IncomingMessage } from "@fncts/http/IncomingMessage";
|
10
|
+
import { ResponseError } from "@fncts/http/ResponseError";
|
11
|
+
import { Server } from "@fncts/http/Server";
|
12
|
+
import { clientAbortFiberId, ServeError } from "@fncts/http/ServerError";
|
13
|
+
import { ServerRequest } from "@fncts/http/ServerRequest";
|
14
|
+
import { Socket } from "@fncts/http/Socket";
|
15
|
+
import { ServerRequestImpl } from "@fncts/node/http/ServerRequest";
|
16
|
+
import * as NodeStream from "@fncts/node/stream";
|
17
|
+
import * as Http from "node:http";
|
18
|
+
import { Readable } from "node:stream";
|
19
|
+
import { pipeline } from "node:stream/promises";
|
20
|
+
import * as ws from "ws";
|
21
|
+
|
22
|
+
export function make(evaluate: Lazy<Http.Server>, options: Net.ListenOptions): IO<Scope, ServeError, Server> {
|
23
|
+
return Do((Δ) => {
|
24
|
+
const scope = Δ(IO.scope);
|
25
|
+
|
26
|
+
const server = Δ(
|
27
|
+
IO.acquireRelease(IO(evaluate), (server) =>
|
28
|
+
IO.async<never, never, void>((resume) => {
|
29
|
+
server.close((error) => {
|
30
|
+
if (error) {
|
31
|
+
resume(IO.haltNow(error));
|
32
|
+
} else {
|
33
|
+
resume(IO.unit);
|
34
|
+
}
|
35
|
+
});
|
36
|
+
}),
|
37
|
+
),
|
38
|
+
);
|
39
|
+
|
40
|
+
Δ(
|
41
|
+
IO.async<never, ServeError, void>((resume) => {
|
42
|
+
server.on("error", (error) => {
|
43
|
+
resume(IO.failNow(new ServeError(error)));
|
44
|
+
});
|
45
|
+
server.listen(options, () => {
|
46
|
+
resume(IO.unit);
|
47
|
+
});
|
48
|
+
}),
|
49
|
+
);
|
50
|
+
|
51
|
+
const address = server.address()!;
|
52
|
+
|
53
|
+
const wss = Δ(
|
54
|
+
scope.extend(
|
55
|
+
IO.acquireRelease(IO(new ws.WebSocketServer({ noServer: true })), (wss) =>
|
56
|
+
IO.async<never, never, void>((resume) => {
|
57
|
+
wss.close(() => resume(IO.unit));
|
58
|
+
}),
|
59
|
+
),
|
60
|
+
).memoize,
|
61
|
+
);
|
62
|
+
|
63
|
+
return Server({
|
64
|
+
address:
|
65
|
+
typeof address === "string"
|
66
|
+
? { _tag: "UnixAddress", path: address }
|
67
|
+
: {
|
68
|
+
_tag: "TcpAddress",
|
69
|
+
hostname: address.address === "::" ? "0.0.0.0" : address.address,
|
70
|
+
port: address.port,
|
71
|
+
},
|
72
|
+
serve: (httpApp, middleware) =>
|
73
|
+
Do((Δ) => {
|
74
|
+
const handler = Δ(makeHandler(httpApp, middleware!));
|
75
|
+
const upgradeHandler = Δ(makeUpgradeHandler(wss, httpApp, middleware!));
|
76
|
+
Δ(
|
77
|
+
IO.addFinalizer(
|
78
|
+
IO(() => {
|
79
|
+
server.off("request", handler);
|
80
|
+
server.off("upgrade", upgradeHandler);
|
81
|
+
}),
|
82
|
+
),
|
83
|
+
);
|
84
|
+
Δ(IO(server.on("request", handler)));
|
85
|
+
Δ(IO(server.on("upgrade", upgradeHandler)));
|
86
|
+
}),
|
87
|
+
});
|
88
|
+
});
|
89
|
+
}
|
90
|
+
|
91
|
+
export function makeHandler<R, E>(
|
92
|
+
httpApp: HttpApp.Default<R, E>,
|
93
|
+
): IO<
|
94
|
+
Exclude<R, ServerRequest | Scope>,
|
95
|
+
never,
|
96
|
+
(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void
|
97
|
+
>;
|
98
|
+
export function makeHandler<R, E, App extends HttpApp.Default<any, any>>(
|
99
|
+
httpApp: HttpApp.Default<R, E>,
|
100
|
+
middleware: Middleware.Applied<R, E | ResponseError, App>,
|
101
|
+
): IO<
|
102
|
+
Exclude<R, ServerRequest | Scope>,
|
103
|
+
never,
|
104
|
+
(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void
|
105
|
+
>;
|
106
|
+
export function makeHandler<R, E, App extends HttpApp.Default<any, any>>(
|
107
|
+
httpApp: HttpApp.Default<R, E>,
|
108
|
+
middleware?: Middleware.Applied<R, E | ResponseError, App>,
|
109
|
+
): IO<
|
110
|
+
Exclude<R, ServerRequest | Scope>,
|
111
|
+
never,
|
112
|
+
(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void
|
113
|
+
> {
|
114
|
+
const handledApp = httpApp.toHandled((request, exit) => {
|
115
|
+
if (exit.isSuccess()) {
|
116
|
+
return handleResponse(request, exit.value).catchAllCause((cause) => handleCause(request, cause));
|
117
|
+
}
|
118
|
+
return handleCause(request, exit.cause);
|
119
|
+
}, middleware as Middleware);
|
120
|
+
|
121
|
+
return FiberSet.makeRuntime<R, any, any>().map((runFork) => {
|
122
|
+
return function handler(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) {
|
123
|
+
const fiber = runFork(
|
124
|
+
handledApp.provideSomeService(new ServerRequestImpl(nodeRequest, nodeResponse), ServerRequest.Tag),
|
125
|
+
);
|
126
|
+
nodeResponse.on("close", () => {
|
127
|
+
if (!nodeResponse.writableEnded) {
|
128
|
+
if (!nodeResponse.headersSent) {
|
129
|
+
nodeResponse.writeHead(499);
|
130
|
+
}
|
131
|
+
nodeResponse.end();
|
132
|
+
fiber.interruptAsFork(clientAbortFiberId).unsafeRunOrFork;
|
133
|
+
}
|
134
|
+
});
|
135
|
+
};
|
136
|
+
}) as IO<
|
137
|
+
Exclude<R, ServerRequest | Scope>,
|
138
|
+
never,
|
139
|
+
(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void
|
140
|
+
>;
|
141
|
+
}
|
142
|
+
|
143
|
+
export function makeUpgradeHandler<R, E>(
|
144
|
+
wss: UIO<ws.WebSocketServer>,
|
145
|
+
httpApp: HttpApp.Default<R, E>,
|
146
|
+
middleware?: Middleware,
|
147
|
+
) {
|
148
|
+
const handledApp = httpApp.toHandled((request, exit) => {
|
149
|
+
if (exit.isSuccess()) {
|
150
|
+
return handleResponse(request, exit.value).catchAllCause((cause) => handleCause(request, cause));
|
151
|
+
}
|
152
|
+
return handleCause(request, exit.cause);
|
153
|
+
}, middleware);
|
154
|
+
|
155
|
+
return FiberSet.makeRuntime<R, any, any>().map((runFork) => {
|
156
|
+
return function handler(nodeRequest: Http.IncomingMessage, socket: Duplex, head: Buffer) {
|
157
|
+
let nodeResponse_: Http.ServerResponse | undefined = undefined;
|
158
|
+
|
159
|
+
const nodeResponse = () => {
|
160
|
+
if (nodeResponse_ === undefined) {
|
161
|
+
nodeResponse_ = new Http.ServerResponse(nodeRequest);
|
162
|
+
nodeResponse_.assignSocket(socket as any);
|
163
|
+
}
|
164
|
+
return nodeResponse_;
|
165
|
+
};
|
166
|
+
|
167
|
+
const upgradeEffect = Socket.fromWebSocket(
|
168
|
+
wss.flatMap((wss) =>
|
169
|
+
IO.acquireRelease(
|
170
|
+
IO.async<never, never, globalThis.WebSocket>((resume) => {
|
171
|
+
wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
|
172
|
+
resume(IO.succeedNow(ws as any));
|
173
|
+
});
|
174
|
+
}),
|
175
|
+
(ws) => IO(ws.close()),
|
176
|
+
),
|
177
|
+
),
|
178
|
+
);
|
179
|
+
|
180
|
+
const fiber = runFork(
|
181
|
+
handledApp.provideSomeService(
|
182
|
+
new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect),
|
183
|
+
ServerRequest.Tag,
|
184
|
+
),
|
185
|
+
);
|
186
|
+
|
187
|
+
socket.on("close", () => {
|
188
|
+
const res = nodeResponse();
|
189
|
+
if (!socket.writableEnded) {
|
190
|
+
if (!res.headersSent) {
|
191
|
+
res.writeHead(499);
|
192
|
+
}
|
193
|
+
res.end();
|
194
|
+
fiber.interruptAsFork(clientAbortFiberId).unsafeRunOrFork;
|
195
|
+
}
|
196
|
+
});
|
197
|
+
};
|
198
|
+
});
|
199
|
+
}
|
200
|
+
|
201
|
+
function handleCause<E>(request: ServerRequest, cause: Cause<E>) {
|
202
|
+
return IO(() => {
|
203
|
+
const nodeResponse = (request as ServerRequestImpl).resolvedResponse;
|
204
|
+
if (!nodeResponse.headersSent) {
|
205
|
+
nodeResponse.writeHead(cause.isInterruptedOnly ? 503 : 500);
|
206
|
+
}
|
207
|
+
if (!nodeResponse.writableEnded) {
|
208
|
+
nodeResponse.end();
|
209
|
+
}
|
210
|
+
});
|
211
|
+
}
|
212
|
+
|
213
|
+
export function handleResponse(request: ServerRequest, response: ServerResponse) {
|
214
|
+
return IO.defer((): IO<never, ResponseError, void> => {
|
215
|
+
const nodeResponse = (request as ServerRequestImpl).resolvedResponse;
|
216
|
+
if (nodeResponse.writableEnded) {
|
217
|
+
return IO.unit;
|
218
|
+
} else if (request.method === "HEAD") {
|
219
|
+
nodeResponse.writeHead(response.status, response.headers.toHeadersInit());
|
220
|
+
nodeResponse.end();
|
221
|
+
return IO.unit;
|
222
|
+
}
|
223
|
+
response.body.concrete();
|
224
|
+
switch (response.body._tag) {
|
225
|
+
case BodyTag.Empty: {
|
226
|
+
nodeResponse.writeHead(response.status, response.headers.toHeadersInit());
|
227
|
+
nodeResponse.end();
|
228
|
+
return IO.unit;
|
229
|
+
}
|
230
|
+
case BodyTag.Raw: {
|
231
|
+
nodeResponse.writeHead(response.status, response.headers.toHeadersInit());
|
232
|
+
if (
|
233
|
+
typeof response.body.body === "object" &&
|
234
|
+
response.body.body !== null &&
|
235
|
+
"pipe" in response.body.body &&
|
236
|
+
typeof response.body.body.pipe === "function"
|
237
|
+
) {
|
238
|
+
return IO.fromPromiseCatch(
|
239
|
+
pipeline(response.body.body as any, nodeResponse, { end: true }),
|
240
|
+
(error) => new ResponseError(request, response, "Decode", error),
|
241
|
+
);
|
242
|
+
}
|
243
|
+
nodeResponse.end(response.body.body);
|
244
|
+
return IO.unit;
|
245
|
+
}
|
246
|
+
case BodyTag.Uint8Array: {
|
247
|
+
nodeResponse.writeHead(response.status, response.headers.toHeadersInit());
|
248
|
+
nodeResponse.end(response.body.body);
|
249
|
+
return IO.unit;
|
250
|
+
}
|
251
|
+
case BodyTag.FormData: {
|
252
|
+
return IO.async<never, ResponseError, void>((resume) => {
|
253
|
+
const r = new Response((response.body as FormData).formData);
|
254
|
+
const headers = {
|
255
|
+
...response.headers,
|
256
|
+
...Object.fromEntries(r.headers as any),
|
257
|
+
};
|
258
|
+
nodeResponse.writeHead(response.status, headers as any);
|
259
|
+
Readable.fromWeb(r.body as any)
|
260
|
+
.pipe(nodeResponse)
|
261
|
+
.on("error", (error) => {
|
262
|
+
resume(IO.failNow(new ResponseError(request, response, "Decode", error)));
|
263
|
+
})
|
264
|
+
.once("finish", () => {
|
265
|
+
resume(IO.unit);
|
266
|
+
});
|
267
|
+
});
|
268
|
+
}
|
269
|
+
case BodyTag.Stream: {
|
270
|
+
nodeResponse.writeHead(response.status, response.headers.toHeadersInit());
|
271
|
+
return response.body.stream
|
272
|
+
.mapError((error) => new ResponseError(request, response, "Decode", error))
|
273
|
+
.run(
|
274
|
+
NodeStream.fromWritable(
|
275
|
+
() => nodeResponse,
|
276
|
+
(error) => new ResponseError(request, response, "Decode", error),
|
277
|
+
),
|
278
|
+
);
|
279
|
+
}
|
280
|
+
}
|
281
|
+
});
|
282
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import type { Headers } from "@fncts/http/Headers";
|
2
|
+
import type { Method } from "@fncts/http/Method";
|
3
|
+
import type { ServerRequest } from "@fncts/http/ServerRequest";
|
4
|
+
import type { Socket } from "@fncts/http/Socket";
|
5
|
+
import type * as Http from "node:http";
|
6
|
+
|
7
|
+
import { RequestError } from "@fncts/http/RequestError";
|
8
|
+
import { ServerRequestTypeId } from "@fncts/http/ServerRequest";
|
9
|
+
import { IncomingMessageImpl } from "@fncts/node/http/IncomingMessage";
|
10
|
+
|
11
|
+
export class ServerRequestImpl extends IncomingMessageImpl<RequestError> implements ServerRequest {
|
12
|
+
readonly [ServerRequestTypeId]: ServerRequestTypeId = ServerRequestTypeId;
|
13
|
+
constructor(
|
14
|
+
readonly source: Http.IncomingMessage,
|
15
|
+
readonly response: Http.ServerResponse | Lazy<Http.ServerResponse>,
|
16
|
+
private upgradeEffect?: IO<never, RequestError, Socket>,
|
17
|
+
readonly url = source.url!,
|
18
|
+
private headersOverride?: Headers,
|
19
|
+
remoteAddressOverride?: string,
|
20
|
+
) {
|
21
|
+
super(source, (error) => new RequestError(this, "Decode", error), remoteAddressOverride);
|
22
|
+
}
|
23
|
+
|
24
|
+
get resolvedResponse(): Http.ServerResponse {
|
25
|
+
return typeof this.response === "function" ? this.response() : this.response;
|
26
|
+
}
|
27
|
+
|
28
|
+
modify(options: {
|
29
|
+
readonly url?: string | undefined;
|
30
|
+
readonly headers?: Headers | undefined;
|
31
|
+
readonly remoteAddress?: string | undefined;
|
32
|
+
}): ServerRequest {
|
33
|
+
return new ServerRequestImpl(
|
34
|
+
this.source,
|
35
|
+
this.response,
|
36
|
+
this.upgradeEffect,
|
37
|
+
options.url ?? this.url,
|
38
|
+
options.headers ?? this.headersOverride,
|
39
|
+
options.remoteAddress ?? this.remoteAddressOverride,
|
40
|
+
);
|
41
|
+
}
|
42
|
+
|
43
|
+
get originalUrl(): string {
|
44
|
+
return this.source.url!;
|
45
|
+
}
|
46
|
+
|
47
|
+
get method(): Method {
|
48
|
+
return this.source.method!.toUpperCase() as Method;
|
49
|
+
}
|
50
|
+
|
51
|
+
get upgrade(): IO<never, RequestError, Socket> {
|
52
|
+
return this.upgradeEffect ?? IO.failNow(new RequestError(this, "Decode", "Not an upgradeable ServerRequest"));
|
53
|
+
}
|
54
|
+
}
|
package/_src/stream/api.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import type { AsyncInputProducer } from "@fncts/io/Channel/internal/AsyncInputProducer";
|
1
2
|
import type stream from "node:stream";
|
2
3
|
|
3
4
|
import { Stream } from "@fncts/io/Stream";
|
@@ -7,66 +8,181 @@ export class ReadableError {
|
|
7
8
|
constructor(readonly error: Error) {}
|
8
9
|
}
|
9
10
|
|
10
|
-
export function fromReadable
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
11
|
+
export function fromReadable<E, A = Uint8Array>(
|
12
|
+
readable: Lazy<stream.Readable | NodeJS.ReadableStream>,
|
13
|
+
onError: (error: unknown) => E,
|
14
|
+
chunkSize?: number,
|
15
|
+
): Stream<never, E, A> {
|
16
|
+
return new Stream<never, E, A>(fromReadableChannel<E, A>(readable, onError, chunkSize));
|
17
|
+
}
|
18
|
+
|
19
|
+
function fromReadableChannel<E, A = Uint8Array>(
|
20
|
+
evaluate: Lazy<stream.Readable | NodeJS.ReadableStream>,
|
21
|
+
onError: (error: unknown) => E,
|
22
|
+
chunkSize: number | undefined,
|
23
|
+
): Channel<never, unknown, unknown, unknown, E, Conc<A>, unknown> {
|
24
|
+
return Channel.acquireReleaseWith(
|
25
|
+
IO(evaluate)
|
26
|
+
.zip(Queue.makeUnbounded<Either<Exit<E, void>, void>>())
|
27
|
+
.tap(([readable, queue]) => readableOffer(readable, queue, onError)),
|
28
|
+
([readable, queue]) => readableTake(readable, queue, chunkSize),
|
29
|
+
([readable, queue]) =>
|
30
|
+
IO(() => {
|
31
|
+
readable.removeAllListeners();
|
32
|
+
if ("closed" in readable && !readable.closed) {
|
33
|
+
readable.destroy();
|
34
|
+
}
|
35
|
+
}) > queue.shutdown,
|
26
36
|
);
|
27
37
|
}
|
28
38
|
|
29
|
-
|
30
|
-
|
31
|
-
|
39
|
+
function readableOffer<E>(
|
40
|
+
readable: stream.Readable | NodeJS.ReadableStream,
|
41
|
+
queue: Queue<Either<Exit<E, void>, void>>,
|
42
|
+
onError: (error: unknown) => E,
|
43
|
+
) {
|
44
|
+
return IO(() => {
|
45
|
+
readable.on("readable", () => {
|
46
|
+
const size = queue.unsafeSize;
|
47
|
+
if (size.isJust() && size.value <= 0) {
|
48
|
+
queue.offer(Either.right(void 0)).unsafeRun;
|
49
|
+
}
|
50
|
+
});
|
51
|
+
readable.on("error", (err) => {
|
52
|
+
queue.unsafeOffer(Either.left(Exit.fail(onError(err))));
|
53
|
+
});
|
54
|
+
readable.on("end", () => {
|
55
|
+
queue.unsafeOffer(Either.left(Exit.unit));
|
56
|
+
});
|
57
|
+
if (readable.readable) {
|
58
|
+
queue.unsafeOffer(Either.right(void 0));
|
59
|
+
}
|
60
|
+
});
|
32
61
|
}
|
33
62
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
writable0.removeListener("error", onError);
|
47
|
-
cb(IO.succeedNow(writable0));
|
48
|
-
});
|
49
|
-
})
|
50
|
-
.acquireRelease((writable) => IO(writable.destroy()))
|
51
|
-
.map((writable) => {
|
52
|
-
const reader: Channel<never, never, Conc<Byte>, unknown, WritableError, never, void> = Channel.readWith(
|
53
|
-
(chunk: Conc<Byte>) =>
|
54
|
-
Channel.unwrap(
|
55
|
-
IO.async<never, WritableError, typeof reader>((cb) => {
|
56
|
-
writable.write(chunk.toBuffer, (err) => {
|
57
|
-
err ? cb(IO.failNow(new WritableError(err))) : cb(IO.succeedNow(reader));
|
58
|
-
});
|
59
|
-
}),
|
60
|
-
),
|
61
|
-
Channel.failNow,
|
63
|
+
function readableTake<E, A>(
|
64
|
+
readable: stream.Readable | NodeJS.ReadableStream,
|
65
|
+
queue: Queue<Either<Exit<E, void>, void>>,
|
66
|
+
chunkSize: number | undefined,
|
67
|
+
) {
|
68
|
+
const read = readChunkChannel<A>(readable, chunkSize);
|
69
|
+
const loop: Channel<never, unknown, unknown, unknown, E, Conc<A>, unknown> = Channel.fromIO(queue.take).flatMap(
|
70
|
+
(either) =>
|
71
|
+
either.match({
|
72
|
+
Left: (exit) =>
|
73
|
+
exit.match(
|
74
|
+
(cause) => Channel.failCauseNow(cause),
|
62
75
|
() => Channel.unit,
|
63
|
-
)
|
64
|
-
|
65
|
-
|
76
|
+
),
|
77
|
+
Right: () => read.flatMap(() => loop),
|
78
|
+
}),
|
79
|
+
);
|
80
|
+
return loop;
|
81
|
+
}
|
82
|
+
|
83
|
+
function readChunkChannel<A>(readable: stream.Readable | NodeJS.ReadableStream, chunkSize: number | undefined) {
|
84
|
+
return Channel.defer(() => {
|
85
|
+
const arr: Array<A> = [];
|
86
|
+
let chunk = readable.read(chunkSize);
|
87
|
+
while (chunk !== null) {
|
88
|
+
arr.push(chunk);
|
89
|
+
chunk = readable.read(chunkSize);
|
90
|
+
}
|
91
|
+
return Channel.write(Conc.fromArray(arr));
|
92
|
+
});
|
93
|
+
}
|
94
|
+
|
95
|
+
export interface FromWritableOptions {
|
96
|
+
readonly endOnDone?: boolean;
|
97
|
+
readonly encoding?: BufferEncoding;
|
98
|
+
}
|
99
|
+
|
100
|
+
export function fromWritable<E, A = Uint8Array | string>(
|
101
|
+
writable: Lazy<stream.Writable | NodeJS.WritableStream>,
|
102
|
+
onError: (error: unknown) => E,
|
103
|
+
options?: FromWritableOptions,
|
104
|
+
): Sink<never, E, A, never, void> {
|
105
|
+
return Sink.fromChannel(fromWritableChannel(writable, onError, options));
|
106
|
+
}
|
107
|
+
|
108
|
+
export function fromWritableChannel<IE, OE, A>(
|
109
|
+
writable: Lazy<stream.Writable | NodeJS.WritableStream>,
|
110
|
+
onError: (error: unknown) => OE,
|
111
|
+
options?: FromWritableOptions,
|
112
|
+
): Channel<never, IE, Conc<A>, unknown, IE | OE, never, void> {
|
113
|
+
return Channel.fromIO(IO(writable).zip(Future.make<IE | OE, void>())).flatMap(([writable, future]) =>
|
114
|
+
writableOutput(writable, future, onError).embedInput(
|
115
|
+
writeInput<IE, A>(writable, (cause) => future.failCause(cause), options, future.fulfill(IO.unit)),
|
66
116
|
),
|
67
117
|
);
|
68
118
|
}
|
69
119
|
|
120
|
+
function writeInput<IE, A>(
|
121
|
+
writable: stream.Writable | NodeJS.WritableStream,
|
122
|
+
onFailure: (cause: Cause<IE>) => UIO<void>,
|
123
|
+
{ encoding, endOnDone = true }: FromWritableOptions = {},
|
124
|
+
onDone = IO.unit,
|
125
|
+
): AsyncInputProducer<IE, Conc<A>, unknown> {
|
126
|
+
const write = writeIO(writable, encoding);
|
127
|
+
const close = endOnDone
|
128
|
+
? IO.async<never, never, void>((resume) => {
|
129
|
+
if ("closed" in writable && writable.closed) {
|
130
|
+
resume(IO.unit);
|
131
|
+
} else {
|
132
|
+
writable.once("finish", () => resume(IO.unit));
|
133
|
+
}
|
134
|
+
})
|
135
|
+
: IO.unit;
|
136
|
+
|
137
|
+
return {
|
138
|
+
awaitRead: IO.unit,
|
139
|
+
emit: write,
|
140
|
+
error: (cause) => close > onFailure(cause),
|
141
|
+
done: (_) => close > onDone,
|
142
|
+
};
|
143
|
+
}
|
144
|
+
|
145
|
+
function writeIO<A>(writable: stream.Writable | NodeJS.WritableStream, encoding?: BufferEncoding) {
|
146
|
+
return (chunk: Conc<A>) => {
|
147
|
+
if (chunk.length === 0) {
|
148
|
+
return IO.unit;
|
149
|
+
} else {
|
150
|
+
return IO.async<never, never, void>((resume) => {
|
151
|
+
const iterator = chunk[Symbol.iterator]();
|
152
|
+
const next = iterator.next();
|
153
|
+
function loop() {
|
154
|
+
const item = next;
|
155
|
+
const success = writable.write(item.value, encoding as any);
|
156
|
+
if (next.done) {
|
157
|
+
resume(IO.unit);
|
158
|
+
} else if (success) {
|
159
|
+
loop();
|
160
|
+
} else {
|
161
|
+
writable.once("drain", loop);
|
162
|
+
}
|
163
|
+
}
|
164
|
+
loop();
|
165
|
+
});
|
166
|
+
}
|
167
|
+
};
|
168
|
+
}
|
169
|
+
|
170
|
+
function writableOutput<IE, OE>(
|
171
|
+
writable: stream.Writable | NodeJS.WritableStream,
|
172
|
+
future: Future<IE | OE, void>,
|
173
|
+
onError: (error: unknown) => OE,
|
174
|
+
) {
|
175
|
+
return Channel.fromIO(
|
176
|
+
IO.defer(() => {
|
177
|
+
function handleError(err: unknown) {
|
178
|
+
future.unsafeDone(IO.failNow(onError(err)));
|
179
|
+
}
|
180
|
+
writable.on("error", handleError);
|
181
|
+
return future.await.ensuring(IO(writable.removeListener("error", handleError)));
|
182
|
+
}),
|
183
|
+
);
|
184
|
+
}
|
185
|
+
|
70
186
|
export class TransformError {
|
71
187
|
readonly _tag = "TransformError";
|
72
188
|
constructor(readonly error: Error) {}
|
@@ -112,3 +228,84 @@ export function transform(transform: Lazy<stream.Transform>) {
|
|
112
228
|
);
|
113
229
|
};
|
114
230
|
}
|
231
|
+
|
232
|
+
export function toString<E>(
|
233
|
+
readable: Lazy<stream.Readable | NodeJS.ReadableStream>,
|
234
|
+
onFailure: (error: unknown) => E,
|
235
|
+
options?: {
|
236
|
+
encoding?: BufferEncoding;
|
237
|
+
maxBytes?: number;
|
238
|
+
},
|
239
|
+
): IO<never, E, string> {
|
240
|
+
const maxBytes = options?.maxBytes ? Number(options.maxBytes) : undefined;
|
241
|
+
return IO(() => {
|
242
|
+
const stream = readable();
|
243
|
+
stream.setEncoding(options?.encoding ?? "utf8");
|
244
|
+
return stream;
|
245
|
+
})
|
246
|
+
.acquireRelease((stream) =>
|
247
|
+
IO(() => {
|
248
|
+
stream.removeAllListeners();
|
249
|
+
if ("closed" in stream && !stream.closed) {
|
250
|
+
stream.destroy();
|
251
|
+
}
|
252
|
+
}),
|
253
|
+
)
|
254
|
+
.flatMap((stream) =>
|
255
|
+
IO.async<never, E, string>((resume) => {
|
256
|
+
let string = "";
|
257
|
+
let bytes = 0;
|
258
|
+
stream.once("error", (err) => {
|
259
|
+
resume(IO.failNow(onFailure(err)));
|
260
|
+
});
|
261
|
+
stream.once("end", () => {
|
262
|
+
resume(IO.succeedNow(string));
|
263
|
+
});
|
264
|
+
stream.on("data", (chunk) => {
|
265
|
+
string += chunk;
|
266
|
+
bytes += Buffer.byteLength(chunk);
|
267
|
+
if (maxBytes && bytes > maxBytes) {
|
268
|
+
resume(IO.failNow(onFailure(new Error("maxBytes exceeded"))));
|
269
|
+
}
|
270
|
+
});
|
271
|
+
}),
|
272
|
+
).scoped;
|
273
|
+
}
|
274
|
+
|
275
|
+
export function toUint8Array<E>(
|
276
|
+
readable: Lazy<stream.Readable | NodeJS.ReadableStream>,
|
277
|
+
onFailure: (error: unknown) => E,
|
278
|
+
options: {
|
279
|
+
maxBytes?: number;
|
280
|
+
} = {},
|
281
|
+
): IO<never, E, Uint8Array> {
|
282
|
+
const maxBytes = options.maxBytes ? Number(options.maxBytes) : undefined;
|
283
|
+
return IO(readable)
|
284
|
+
.acquireRelease((stream) =>
|
285
|
+
IO(() => {
|
286
|
+
stream.removeAllListeners();
|
287
|
+
if ("closed" in stream && !stream.closed) {
|
288
|
+
stream.destroy();
|
289
|
+
}
|
290
|
+
}),
|
291
|
+
)
|
292
|
+
.flatMap((stream) =>
|
293
|
+
IO.async<never, E, Uint8Array>((resume) => {
|
294
|
+
let buffer = Buffer.alloc(0);
|
295
|
+
let bytes = 0;
|
296
|
+
stream.once("error", (err) => {
|
297
|
+
resume(IO.failNow(onFailure(err)));
|
298
|
+
});
|
299
|
+
stream.once("end", () => {
|
300
|
+
resume(IO.succeedNow(buffer));
|
301
|
+
});
|
302
|
+
stream.on("data", (chunk) => {
|
303
|
+
buffer = Buffer.concat([buffer, chunk]);
|
304
|
+
bytes += chunk.length;
|
305
|
+
if (maxBytes && bytes > maxBytes) {
|
306
|
+
resume(IO.failNow(onFailure(new Error("maxBytes exceeded"))));
|
307
|
+
}
|
308
|
+
});
|
309
|
+
}),
|
310
|
+
).scoped;
|
311
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
/// <reference types="node" />
|
2
|
+
import { IO } from "@fncts/io/IO/definition";
|
3
|
+
import { Stream } from "@fncts/io/Stream/definition";
|
4
|
+
import type * as Http from "node:http";
|
5
|
+
import { Maybe } from "@fncts/base/data/Maybe";
|
6
|
+
import { Headers } from "@fncts/http/Headers";
|
7
|
+
import { IncomingMessage } from "@fncts/http/IncomingMessage";
|
8
|
+
import { UrlParams } from "@fncts/http/UrlParams";
|
9
|
+
export declare abstract class IncomingMessageImpl<E> extends IncomingMessage<E> {
|
10
|
+
readonly source: Http.IncomingMessage;
|
11
|
+
readonly onError: (error: unknown) => E;
|
12
|
+
readonly remoteAddressOverride?: string | undefined;
|
13
|
+
constructor(source: Http.IncomingMessage, onError: (error: unknown) => E, remoteAddressOverride?: string | undefined);
|
14
|
+
get headers(): Headers;
|
15
|
+
get remoteAddress(): Maybe<string>;
|
16
|
+
private textIO;
|
17
|
+
get text(): IO<never, E, string>;
|
18
|
+
get json(): IO<never, E, unknown>;
|
19
|
+
get urlParamsBody(): IO<never, E, UrlParams>;
|
20
|
+
private arrayBufferIO;
|
21
|
+
get arrayBuffer(): IO<never, E, ArrayBuffer>;
|
22
|
+
get stream(): Stream<never, E, Uint8Array>;
|
23
|
+
}
|