effect-start 0.20.0 → 0.21.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/package.json +6 -6
- package/src/ContentNegotiation.ts +8 -9
- package/src/Cookies.ts +429 -0
- package/src/Development.ts +1 -1
- package/src/FileRouter.ts +1 -1
- package/src/FileRouterCodegen.ts +1 -1
- package/src/FileSystem.ts +403 -0
- package/src/PlatformError.ts +3 -3
- package/src/Socket.ts +48 -0
- package/src/Start.ts +1 -2
- package/src/bun/BunServer.ts +58 -32
- package/src/bundler/BundleFiles.ts +1 -1
- package/src/experimental/EncryptedCookies.ts +3 -34
- package/src/experimental/index.ts +0 -1
- package/src/hyper/HyperHtml.ts +4 -0
- package/src/node/NodeFileSystem.ts +2 -16
- package/src/testing/index.ts +0 -1
- package/src/x/cloudflare/CloudflareTunnel.ts +28 -23
- package/src/HttpAppExtra.ts +0 -478
- package/src/HttpUtils.ts +0 -17
- package/src/bun/BunPlatformHttpServer.ts +0 -88
- package/src/bun/BunServerRequest.ts +0 -396
- package/src/bundler/BundleHttp.ts +0 -259
- package/src/experimental/SseHttpResponse.ts +0 -55
- package/src/middlewares/BasicAuthMiddleware.ts +0 -36
- package/src/middlewares/index.ts +0 -1
- package/src/testing/TestHttpClient.ts +0 -148
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from @effect/platform
|
|
3
|
+
*/
|
|
4
|
+
import * as Brand from "effect/Brand"
|
|
5
|
+
import * as Channel from "effect/Channel"
|
|
6
|
+
import * as Chunk from "effect/Chunk"
|
|
7
|
+
import * as Context from "effect/Context"
|
|
8
|
+
import * as Data from "effect/Data"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Function from "effect/Function"
|
|
11
|
+
import * as Option from "effect/Option"
|
|
12
|
+
import * as Sink from "effect/Sink"
|
|
13
|
+
import type * as Scope from "effect/Scope"
|
|
14
|
+
import * as Stream from "effect/Stream"
|
|
15
|
+
import * as PlatformError from "./PlatformError.ts"
|
|
16
|
+
|
|
17
|
+
export interface FileSystem {
|
|
18
|
+
readonly access: (
|
|
19
|
+
path: string,
|
|
20
|
+
options?: AccessFileOptions,
|
|
21
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
22
|
+
readonly copy: (
|
|
23
|
+
fromPath: string,
|
|
24
|
+
toPath: string,
|
|
25
|
+
options?: CopyOptions,
|
|
26
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
27
|
+
readonly copyFile: (
|
|
28
|
+
fromPath: string,
|
|
29
|
+
toPath: string,
|
|
30
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
31
|
+
readonly chmod: (
|
|
32
|
+
path: string,
|
|
33
|
+
mode: number,
|
|
34
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
35
|
+
readonly chown: (
|
|
36
|
+
path: string,
|
|
37
|
+
uid: number,
|
|
38
|
+
gid: number,
|
|
39
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
40
|
+
readonly exists: (
|
|
41
|
+
path: string,
|
|
42
|
+
) => Effect.Effect<boolean, PlatformError.PlatformError>
|
|
43
|
+
readonly link: (
|
|
44
|
+
fromPath: string,
|
|
45
|
+
toPath: string,
|
|
46
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
47
|
+
readonly makeDirectory: (
|
|
48
|
+
path: string,
|
|
49
|
+
options?: MakeDirectoryOptions,
|
|
50
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
51
|
+
readonly makeTempDirectory: (
|
|
52
|
+
options?: MakeTempDirectoryOptions,
|
|
53
|
+
) => Effect.Effect<string, PlatformError.PlatformError>
|
|
54
|
+
readonly makeTempDirectoryScoped: (
|
|
55
|
+
options?: MakeTempDirectoryOptions,
|
|
56
|
+
) => Effect.Effect<string, PlatformError.PlatformError, Scope.Scope>
|
|
57
|
+
readonly makeTempFile: (
|
|
58
|
+
options?: MakeTempFileOptions,
|
|
59
|
+
) => Effect.Effect<string, PlatformError.PlatformError>
|
|
60
|
+
readonly makeTempFileScoped: (
|
|
61
|
+
options?: MakeTempFileOptions,
|
|
62
|
+
) => Effect.Effect<string, PlatformError.PlatformError, Scope.Scope>
|
|
63
|
+
readonly open: (
|
|
64
|
+
path: string,
|
|
65
|
+
options?: OpenFileOptions,
|
|
66
|
+
) => Effect.Effect<File, PlatformError.PlatformError, Scope.Scope>
|
|
67
|
+
readonly readDirectory: (
|
|
68
|
+
path: string,
|
|
69
|
+
options?: ReadDirectoryOptions,
|
|
70
|
+
) => Effect.Effect<Array<string>, PlatformError.PlatformError>
|
|
71
|
+
readonly readFile: (
|
|
72
|
+
path: string,
|
|
73
|
+
) => Effect.Effect<Uint8Array, PlatformError.PlatformError>
|
|
74
|
+
readonly readFileString: (
|
|
75
|
+
path: string,
|
|
76
|
+
encoding?: string,
|
|
77
|
+
) => Effect.Effect<string, PlatformError.PlatformError>
|
|
78
|
+
readonly readLink: (
|
|
79
|
+
path: string,
|
|
80
|
+
) => Effect.Effect<string, PlatformError.PlatformError>
|
|
81
|
+
readonly realPath: (
|
|
82
|
+
path: string,
|
|
83
|
+
) => Effect.Effect<string, PlatformError.PlatformError>
|
|
84
|
+
readonly remove: (
|
|
85
|
+
path: string,
|
|
86
|
+
options?: RemoveOptions,
|
|
87
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
88
|
+
readonly rename: (
|
|
89
|
+
oldPath: string,
|
|
90
|
+
newPath: string,
|
|
91
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
92
|
+
readonly sink: (
|
|
93
|
+
path: string,
|
|
94
|
+
options?: SinkOptions,
|
|
95
|
+
) => Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>
|
|
96
|
+
readonly stat: (
|
|
97
|
+
path: string,
|
|
98
|
+
) => Effect.Effect<File.Info, PlatformError.PlatformError>
|
|
99
|
+
readonly stream: (
|
|
100
|
+
path: string,
|
|
101
|
+
options?: StreamOptions,
|
|
102
|
+
) => Stream.Stream<Uint8Array, PlatformError.PlatformError>
|
|
103
|
+
readonly symlink: (
|
|
104
|
+
fromPath: string,
|
|
105
|
+
toPath: string,
|
|
106
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
107
|
+
readonly truncate: (
|
|
108
|
+
path: string,
|
|
109
|
+
length?: SizeInput,
|
|
110
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
111
|
+
readonly utimes: (
|
|
112
|
+
path: string,
|
|
113
|
+
atime: Date | number,
|
|
114
|
+
mtime: Date | number,
|
|
115
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
116
|
+
readonly watch: (
|
|
117
|
+
path: string,
|
|
118
|
+
options?: WatchOptions,
|
|
119
|
+
) => Stream.Stream<WatchEvent, PlatformError.PlatformError>
|
|
120
|
+
readonly writeFile: (
|
|
121
|
+
path: string,
|
|
122
|
+
data: Uint8Array,
|
|
123
|
+
options?: WriteFileOptions,
|
|
124
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
125
|
+
readonly writeFileString: (
|
|
126
|
+
path: string,
|
|
127
|
+
data: string,
|
|
128
|
+
options?: WriteFileStringOptions,
|
|
129
|
+
) => Effect.Effect<void, PlatformError.PlatformError>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const FileSystem: Context.Tag<FileSystem, FileSystem> = Context.GenericTag<FileSystem>(
|
|
133
|
+
"@effect/platform/FileSystem",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
export type Size = Brand.Branded<bigint, "Size">
|
|
137
|
+
|
|
138
|
+
export type SizeInput = bigint | number | Size
|
|
139
|
+
|
|
140
|
+
export const Size = (bytes: SizeInput): Size =>
|
|
141
|
+
typeof bytes === "bigint" ? bytes as Size : BigInt(bytes) as Size
|
|
142
|
+
|
|
143
|
+
export type OpenFlag =
|
|
144
|
+
| "r"
|
|
145
|
+
| "r+"
|
|
146
|
+
| "w"
|
|
147
|
+
| "wx"
|
|
148
|
+
| "w+"
|
|
149
|
+
| "wx+"
|
|
150
|
+
| "a"
|
|
151
|
+
| "ax"
|
|
152
|
+
| "a+"
|
|
153
|
+
| "ax+"
|
|
154
|
+
|
|
155
|
+
export type SeekMode = "start" | "current"
|
|
156
|
+
|
|
157
|
+
export interface AccessFileOptions {
|
|
158
|
+
readonly ok?: boolean
|
|
159
|
+
readonly readable?: boolean
|
|
160
|
+
readonly writable?: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface MakeDirectoryOptions {
|
|
164
|
+
readonly recursive?: boolean
|
|
165
|
+
readonly mode?: number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface CopyOptions {
|
|
169
|
+
readonly overwrite?: boolean
|
|
170
|
+
readonly preserveTimestamps?: boolean
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface MakeTempDirectoryOptions {
|
|
174
|
+
readonly directory?: string
|
|
175
|
+
readonly prefix?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface MakeTempFileOptions {
|
|
179
|
+
readonly directory?: string
|
|
180
|
+
readonly prefix?: string
|
|
181
|
+
readonly suffix?: string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface OpenFileOptions {
|
|
185
|
+
readonly flag?: OpenFlag
|
|
186
|
+
readonly mode?: number
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ReadDirectoryOptions {
|
|
190
|
+
readonly recursive?: boolean
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface RemoveOptions {
|
|
194
|
+
readonly recursive?: boolean
|
|
195
|
+
readonly force?: boolean
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface SinkOptions extends OpenFileOptions {}
|
|
199
|
+
|
|
200
|
+
export interface StreamOptions {
|
|
201
|
+
readonly bufferSize?: number
|
|
202
|
+
readonly bytesToRead?: SizeInput
|
|
203
|
+
readonly chunkSize?: SizeInput
|
|
204
|
+
readonly offset?: SizeInput
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface WriteFileOptions {
|
|
208
|
+
readonly flag?: OpenFlag
|
|
209
|
+
readonly mode?: number
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface WriteFileStringOptions {
|
|
213
|
+
readonly flag?: OpenFlag
|
|
214
|
+
readonly mode?: number
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface WatchOptions {
|
|
218
|
+
readonly recursive?: boolean
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const FileTypeId: unique symbol = Symbol.for("@effect/platform/FileSystem/File")
|
|
222
|
+
|
|
223
|
+
export type FileTypeId = typeof FileTypeId
|
|
224
|
+
|
|
225
|
+
export interface File {
|
|
226
|
+
readonly [FileTypeId]: FileTypeId
|
|
227
|
+
readonly fd: File.Descriptor
|
|
228
|
+
readonly stat: Effect.Effect<File.Info, PlatformError.PlatformError>
|
|
229
|
+
readonly seek: (offset: SizeInput, from: SeekMode) => Effect.Effect<void>
|
|
230
|
+
readonly sync: Effect.Effect<void, PlatformError.PlatformError>
|
|
231
|
+
readonly read: (buffer: Uint8Array) => Effect.Effect<Size, PlatformError.PlatformError>
|
|
232
|
+
readonly readAlloc: (size: SizeInput) => Effect.Effect<Option.Option<Uint8Array>, PlatformError.PlatformError>
|
|
233
|
+
readonly truncate: (length?: SizeInput) => Effect.Effect<void, PlatformError.PlatformError>
|
|
234
|
+
readonly write: (buffer: Uint8Array) => Effect.Effect<Size, PlatformError.PlatformError>
|
|
235
|
+
readonly writeAll: (buffer: Uint8Array) => Effect.Effect<void, PlatformError.PlatformError>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export declare namespace File {
|
|
239
|
+
export type Descriptor = Brand.Branded<number, "FileDescriptor">
|
|
240
|
+
|
|
241
|
+
export type Type =
|
|
242
|
+
| "File"
|
|
243
|
+
| "Directory"
|
|
244
|
+
| "SymbolicLink"
|
|
245
|
+
| "BlockDevice"
|
|
246
|
+
| "CharacterDevice"
|
|
247
|
+
| "FIFO"
|
|
248
|
+
| "Socket"
|
|
249
|
+
| "Unknown"
|
|
250
|
+
|
|
251
|
+
export interface Info {
|
|
252
|
+
readonly type: Type
|
|
253
|
+
readonly mtime: Option.Option<Date>
|
|
254
|
+
readonly atime: Option.Option<Date>
|
|
255
|
+
readonly birthtime: Option.Option<Date>
|
|
256
|
+
readonly dev: number
|
|
257
|
+
readonly ino: Option.Option<number>
|
|
258
|
+
readonly mode: number
|
|
259
|
+
readonly nlink: Option.Option<number>
|
|
260
|
+
readonly uid: Option.Option<number>
|
|
261
|
+
readonly gid: Option.Option<number>
|
|
262
|
+
readonly rdev: Option.Option<number>
|
|
263
|
+
readonly size: Size
|
|
264
|
+
readonly blksize: Option.Option<Size>
|
|
265
|
+
readonly blocks: Option.Option<number>
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const FileDescriptor = Brand.nominal<File.Descriptor>()
|
|
270
|
+
|
|
271
|
+
export type WatchEvent = WatchEvent.Create | WatchEvent.Update | WatchEvent.Remove
|
|
272
|
+
|
|
273
|
+
export declare namespace WatchEvent {
|
|
274
|
+
export interface Create {
|
|
275
|
+
readonly _tag: "Create"
|
|
276
|
+
readonly path: string
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface Update {
|
|
280
|
+
readonly _tag: "Update"
|
|
281
|
+
readonly path: string
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface Remove {
|
|
285
|
+
readonly _tag: "Remove"
|
|
286
|
+
readonly path: string
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export const WatchEventCreate: Data.Case.Constructor<WatchEvent.Create, "_tag"> = Data.tagged<WatchEvent.Create>(
|
|
291
|
+
"Create",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
export const WatchEventUpdate: Data.Case.Constructor<WatchEvent.Update, "_tag"> = Data.tagged<WatchEvent.Update>(
|
|
295
|
+
"Update",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
export const WatchEventRemove: Data.Case.Constructor<WatchEvent.Remove, "_tag"> = Data.tagged<WatchEvent.Remove>(
|
|
299
|
+
"Remove",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
export class WatchBackend extends Context.Tag("@effect/platform/FileSystem/WatchBackend")<
|
|
303
|
+
WatchBackend,
|
|
304
|
+
{
|
|
305
|
+
readonly register: (
|
|
306
|
+
path: string,
|
|
307
|
+
stat: File.Info,
|
|
308
|
+
options?: WatchOptions,
|
|
309
|
+
) => Option.Option<Stream.Stream<WatchEvent, PlatformError.PlatformError>>
|
|
310
|
+
}
|
|
311
|
+
>() {}
|
|
312
|
+
|
|
313
|
+
export const make = (
|
|
314
|
+
impl: Omit<FileSystem, "exists" | "readFileString" | "stream" | "sink" | "writeFileString">,
|
|
315
|
+
): FileSystem => {
|
|
316
|
+
return FileSystem.of({
|
|
317
|
+
...impl,
|
|
318
|
+
exists: (path) =>
|
|
319
|
+
Function.pipe(
|
|
320
|
+
impl.access(path),
|
|
321
|
+
Effect.as(true),
|
|
322
|
+
Effect.catchTag("SystemError", (e) => e.reason === "NotFound" ? Effect.succeed(false) : Effect.fail(e)),
|
|
323
|
+
),
|
|
324
|
+
readFileString: (path, encoding) =>
|
|
325
|
+
Effect.tryMap(impl.readFile(path), {
|
|
326
|
+
try: (_) => new TextDecoder(encoding).decode(_),
|
|
327
|
+
catch: (cause) =>
|
|
328
|
+
new PlatformError.BadArgument({
|
|
329
|
+
module: "FileSystem",
|
|
330
|
+
method: "readFileString",
|
|
331
|
+
description: "invalid encoding",
|
|
332
|
+
cause,
|
|
333
|
+
}),
|
|
334
|
+
}),
|
|
335
|
+
stream: (path, options) =>
|
|
336
|
+
Function.pipe(
|
|
337
|
+
impl.open(path, { flag: "r" }),
|
|
338
|
+
options?.offset
|
|
339
|
+
? Effect.tap((file) => file.seek(options.offset!, "start"))
|
|
340
|
+
: Function.identity,
|
|
341
|
+
Effect.map((file) => fileStream(file, options)),
|
|
342
|
+
Stream.unwrapScoped,
|
|
343
|
+
),
|
|
344
|
+
sink: (path, options) =>
|
|
345
|
+
Function.pipe(
|
|
346
|
+
impl.open(path, { flag: "w", ...options }),
|
|
347
|
+
Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))),
|
|
348
|
+
Sink.unwrapScoped,
|
|
349
|
+
),
|
|
350
|
+
writeFileString: (path, data, options) =>
|
|
351
|
+
Effect.flatMap(
|
|
352
|
+
Effect.try({
|
|
353
|
+
try: () => new TextEncoder().encode(data),
|
|
354
|
+
catch: (cause) =>
|
|
355
|
+
new PlatformError.BadArgument({
|
|
356
|
+
module: "FileSystem",
|
|
357
|
+
method: "writeFileString",
|
|
358
|
+
description: "could not encode string",
|
|
359
|
+
cause,
|
|
360
|
+
}),
|
|
361
|
+
}),
|
|
362
|
+
(_) => impl.writeFile(path, _, options),
|
|
363
|
+
),
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const fileStream = (file: File, {
|
|
368
|
+
bufferSize = 16,
|
|
369
|
+
bytesToRead: bytesToRead_,
|
|
370
|
+
chunkSize: chunkSize_ = Size(64 * 1024),
|
|
371
|
+
}: StreamOptions = {}) => {
|
|
372
|
+
const bytesToRead = bytesToRead_ !== undefined ? Size(bytesToRead_) : undefined
|
|
373
|
+
const chunkSize = Size(chunkSize_)
|
|
374
|
+
|
|
375
|
+
function loop(
|
|
376
|
+
totalBytesRead: bigint,
|
|
377
|
+
): Channel.Channel<Chunk.Chunk<Uint8Array>, unknown, PlatformError.PlatformError, unknown, void, unknown> {
|
|
378
|
+
if (bytesToRead !== undefined && bytesToRead <= totalBytesRead) {
|
|
379
|
+
return Channel.void
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const toRead = bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize
|
|
383
|
+
? bytesToRead - totalBytesRead
|
|
384
|
+
: chunkSize
|
|
385
|
+
|
|
386
|
+
return Channel.flatMap(
|
|
387
|
+
file.readAlloc(toRead),
|
|
388
|
+
Option.match({
|
|
389
|
+
onNone: () => Channel.void,
|
|
390
|
+
onSome: (buf) =>
|
|
391
|
+
Channel.flatMap(
|
|
392
|
+
Channel.write(Chunk.of(buf)),
|
|
393
|
+
() => loop(totalBytesRead + BigInt(buf.length)),
|
|
394
|
+
),
|
|
395
|
+
}),
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return Stream.bufferChunks(
|
|
400
|
+
Stream.fromChannel(loop(BigInt(0))),
|
|
401
|
+
{ capacity: bufferSize },
|
|
402
|
+
)
|
|
403
|
+
}
|
package/src/PlatformError.ts
CHANGED
|
@@ -7,9 +7,9 @@ import * as Predicate from "effect/Predicate"
|
|
|
7
7
|
import * as Schema from "effect/Schema"
|
|
8
8
|
import type * as Types from "effect/Types"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export const TypeId: unique symbol = Symbol.for(
|
|
11
|
+
"@effect/platform/Error/PlatformError",
|
|
12
|
+
)
|
|
13
13
|
|
|
14
14
|
export type TypeId = typeof TypeId
|
|
15
15
|
|
package/src/Socket.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from @effect/platform
|
|
3
|
+
*/
|
|
4
|
+
import * as Predicate from "effect/Predicate"
|
|
5
|
+
import * as PlatformError from "./PlatformError.ts"
|
|
6
|
+
|
|
7
|
+
export const SocketErrorTypeId: unique symbol = Symbol.for("@effect/platform/Socket/SocketError")
|
|
8
|
+
|
|
9
|
+
export type SocketErrorTypeId = typeof SocketErrorTypeId
|
|
10
|
+
|
|
11
|
+
export const isSocketError = (u: unknown): u is SocketError =>
|
|
12
|
+
Predicate.hasProperty(u, SocketErrorTypeId)
|
|
13
|
+
|
|
14
|
+
export type SocketError = SocketGenericError | SocketCloseError
|
|
15
|
+
|
|
16
|
+
export class SocketGenericError extends PlatformError.TypeIdError(SocketErrorTypeId, "SocketError")<{
|
|
17
|
+
readonly reason: "Write" | "Read" | "Open" | "OpenTimeout"
|
|
18
|
+
readonly cause: unknown
|
|
19
|
+
}> {
|
|
20
|
+
get message() {
|
|
21
|
+
return `An error occurred during ${this.reason}`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class SocketCloseError extends PlatformError.TypeIdError(SocketErrorTypeId, "SocketError")<{
|
|
26
|
+
readonly reason: "Close"
|
|
27
|
+
readonly code: number
|
|
28
|
+
readonly closeReason?: string | undefined
|
|
29
|
+
}> {
|
|
30
|
+
static is(u: unknown): u is SocketCloseError {
|
|
31
|
+
return isSocketError(u) && u.reason === "Close"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static isClean(isClean: (code: number) => boolean) {
|
|
35
|
+
return function(u: unknown): u is SocketCloseError {
|
|
36
|
+
return SocketCloseError.is(u) && isClean(u.code)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get message() {
|
|
41
|
+
if (this.closeReason) {
|
|
42
|
+
return `${this.reason}: ${this.code}: ${this.closeReason}`
|
|
43
|
+
}
|
|
44
|
+
return `${this.reason}: ${this.code}`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const defaultCloseCodeIsError = (code: number) => code !== 1000 && code !== 1006
|
package/src/Start.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as FileSystem from "
|
|
1
|
+
import * as FileSystem from "./FileSystem.ts"
|
|
2
2
|
import * as Context from "effect/Context"
|
|
3
3
|
import * as Effect from "effect/Effect"
|
|
4
4
|
import * as Function from "effect/Function"
|
|
@@ -84,7 +84,6 @@ export function serve<
|
|
|
84
84
|
BunServer.withLogAddress,
|
|
85
85
|
Layer.provide(appLayer),
|
|
86
86
|
Layer.provide(NodeFileSystem.layer),
|
|
87
|
-
Layer.provide(BunServer.layer()),
|
|
88
87
|
) as Layer.Layer<BunServer.BunServer, never, never>
|
|
89
88
|
|
|
90
89
|
return Function.pipe(
|
package/src/bun/BunServer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as Socket from "
|
|
1
|
+
import * as Socket from "../Socket.ts"
|
|
2
2
|
import * as Bun from "bun"
|
|
3
3
|
import * as Config from "effect/Config"
|
|
4
4
|
import * as Context from "effect/Context"
|
|
@@ -9,6 +9,7 @@ import * as Layer from "effect/Layer"
|
|
|
9
9
|
import * as Option from "effect/Option"
|
|
10
10
|
import * as Runtime from "effect/Runtime"
|
|
11
11
|
import type * as Scope from "effect/Scope"
|
|
12
|
+
import * as NOs from "node:os"
|
|
12
13
|
import * as PathPattern from "../PathPattern.ts"
|
|
13
14
|
import * as PlataformRuntime from "../PlatformRuntime.ts"
|
|
14
15
|
import * as Route from "../Route.ts"
|
|
@@ -16,11 +17,16 @@ import * as RouteHttp from "../RouteHttp.ts"
|
|
|
16
17
|
import * as RouteMount from "../RouteMount.ts"
|
|
17
18
|
import * as RouteTree from "../RouteTree.ts"
|
|
18
19
|
import * as BunRoute from "./BunRoute.ts"
|
|
19
|
-
|
|
20
|
+
export interface WebSocketContext {
|
|
21
|
+
readonly deferred: Deferred.Deferred<Bun.ServerWebSocket<WebSocketContext>>
|
|
22
|
+
readonly closeDeferred: Deferred.Deferred<void, Socket.SocketError>
|
|
23
|
+
readonly buffer: Array<Uint8Array | string>
|
|
24
|
+
run: (_: Uint8Array | string) => void
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
type FetchHandler = (
|
|
22
28
|
request: Request,
|
|
23
|
-
server: Bun.Server<
|
|
29
|
+
server: Bun.Server<WebSocketContext>,
|
|
24
30
|
) => Response | Promise<Response>
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -38,7 +44,7 @@ interface BunServeOptions {
|
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export type BunServer = {
|
|
41
|
-
readonly server: Bun.Server<
|
|
47
|
+
readonly server: Bun.Server<WebSocketContext>
|
|
42
48
|
readonly pushHandler: (fetch: FetchHandler) => void
|
|
43
49
|
readonly popHandler: () => void
|
|
44
50
|
}
|
|
@@ -66,8 +72,10 @@ export const make = (
|
|
|
66
72
|
: Effect.succeed(3000)
|
|
67
73
|
}),
|
|
68
74
|
)
|
|
75
|
+
const hostFlag = process.argv.includes("--host")
|
|
69
76
|
const hostname = yield* Config.string("HOSTNAME").pipe(
|
|
70
|
-
Effect.catchTag("ConfigError", () =>
|
|
77
|
+
Effect.catchTag("ConfigError", () =>
|
|
78
|
+
Effect.succeed(hostFlag ? "0.0.0.0" : undefined)),
|
|
71
79
|
)
|
|
72
80
|
|
|
73
81
|
const handlerStack: Array<FetchHandler> = [
|
|
@@ -76,35 +84,43 @@ export const make = (
|
|
|
76
84
|
},
|
|
77
85
|
]
|
|
78
86
|
|
|
79
|
-
const service = BunServer
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
const service = BunServer
|
|
88
|
+
.of({
|
|
89
|
+
// During the construction we need to create a service imlpementation
|
|
90
|
+
// first so we can provide it in the runtime that will be used in web
|
|
91
|
+
// handlers. After we create the runtime, we set it below so it's always
|
|
92
|
+
// available at runtime.
|
|
93
|
+
// An alternative approach would be to use Bun.Server.reload but I prefer
|
|
94
|
+
// to avoid it since it's badly documented and has bunch of bugs.
|
|
95
|
+
server: undefined as any,
|
|
96
|
+
pushHandler(fetch) {
|
|
97
|
+
handlerStack
|
|
98
|
+
.push(fetch)
|
|
99
|
+
reload()
|
|
100
|
+
},
|
|
101
|
+
popHandler() {
|
|
102
|
+
handlerStack
|
|
103
|
+
.pop()
|
|
104
|
+
reload()
|
|
105
|
+
},
|
|
106
|
+
})
|
|
96
107
|
|
|
97
|
-
const runtime = yield* Effect
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
const runtime = yield* Effect
|
|
109
|
+
.runtime()
|
|
110
|
+
.pipe(
|
|
111
|
+
Effect
|
|
112
|
+
.andThen(Runtime
|
|
113
|
+
.provideService(BunServer, service)),
|
|
114
|
+
)
|
|
100
115
|
|
|
101
116
|
let currentRoutes: BunRoute.BunRoutes = routes
|
|
102
117
|
? yield* walkBunRoutes(runtime, routes)
|
|
103
118
|
: {}
|
|
104
119
|
|
|
105
|
-
const websocket: Bun.WebSocketHandler<
|
|
120
|
+
const websocket: Bun.WebSocketHandler<WebSocketContext> = {
|
|
106
121
|
open(ws) {
|
|
107
|
-
Deferred
|
|
122
|
+
Deferred
|
|
123
|
+
.unsafeDone(ws.data.deferred, Exit.succeed(ws))
|
|
108
124
|
},
|
|
109
125
|
message(ws, message) {
|
|
110
126
|
ws.data.run(message)
|
|
@@ -183,11 +199,14 @@ export const withLogAddress = <A, E, R>(
|
|
|
183
199
|
Layer
|
|
184
200
|
.effectDiscard(
|
|
185
201
|
BunServer.pipe(
|
|
186
|
-
Effect.andThen(server =>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
Effect.andThen(server => {
|
|
203
|
+
const { hostname, port } = server.server
|
|
204
|
+
const addr = hostname === "0.0.0.0"
|
|
205
|
+
? getLocalIp()
|
|
206
|
+
: "localhost"
|
|
207
|
+
|
|
208
|
+
return Effect.log(`Listening on http://${addr}:${port}`)
|
|
209
|
+
}),
|
|
191
210
|
),
|
|
192
211
|
)
|
|
193
212
|
.pipe(
|
|
@@ -226,3 +245,10 @@ function walkBunRoutes(
|
|
|
226
245
|
return bunRoutes
|
|
227
246
|
})
|
|
228
247
|
}
|
|
248
|
+
|
|
249
|
+
function getLocalIp(): string | undefined {
|
|
250
|
+
return Object.values(NOs.networkInterfaces())
|
|
251
|
+
.flatMap(addresses => addresses ?? [])
|
|
252
|
+
.find(addr => addr.family === "IPv4" && !addr.internal)
|
|
253
|
+
?.address
|
|
254
|
+
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
Cookies,
|
|
3
|
-
HttpApp,
|
|
4
|
-
HttpServerResponse,
|
|
5
|
-
} from "@effect/platform"
|
|
6
|
-
import {
|
|
7
|
-
Effect,
|
|
8
|
-
pipe,
|
|
9
|
-
} from "effect"
|
|
1
|
+
import * as Cookies from "../Cookies.ts"
|
|
10
2
|
import * as Config from "effect/Config"
|
|
11
3
|
import * as Context from "effect/Context"
|
|
12
4
|
import * as Data from "effect/Data"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
13
6
|
import * as Layer from "effect/Layer"
|
|
14
7
|
|
|
15
8
|
type CookieValue =
|
|
@@ -79,8 +72,7 @@ export function layer(options: { secret: string }) {
|
|
|
79
72
|
export function layerConfig(name = "SECRET_KEY_BASE") {
|
|
80
73
|
return Effect
|
|
81
74
|
.gen(function*() {
|
|
82
|
-
const secret = yield* pipe(
|
|
83
|
-
Config.nonEmptyString(name),
|
|
75
|
+
const secret = yield* Config.nonEmptyString(name).pipe(
|
|
84
76
|
Effect.flatMap((value) => {
|
|
85
77
|
return (value.length < 40)
|
|
86
78
|
? Effect.fail(new Error("ba"))
|
|
@@ -426,26 +418,3 @@ function deriveKey(
|
|
|
426
418
|
return key
|
|
427
419
|
})
|
|
428
420
|
}
|
|
429
|
-
|
|
430
|
-
// TODO something si wrong with return type
|
|
431
|
-
export function handleError<E>(
|
|
432
|
-
app: HttpApp.Default<E | EncryptedCookiesError>,
|
|
433
|
-
) {
|
|
434
|
-
return Effect.gen(function*() {
|
|
435
|
-
const res = yield* app.pipe(
|
|
436
|
-
Effect.catchTag("EncryptedCookiesError", (error) => {
|
|
437
|
-
return HttpServerResponse.empty()
|
|
438
|
-
}),
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
return res
|
|
442
|
-
})
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function generateFriendlyKey(bits = 128) {
|
|
446
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
447
|
-
const length = Math.ceil(bits / Math.log2(chars.length))
|
|
448
|
-
const bytes = crypto.getRandomValues(new Uint8Array(length))
|
|
449
|
-
|
|
450
|
-
return Array.from(bytes, b => chars[b % chars.length]).join("")
|
|
451
|
-
}
|