effect-start 0.15.0 → 0.17.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.
Files changed (46) hide show
  1. package/package.json +2 -1
  2. package/src/ContentNegotiation.test.ts +103 -0
  3. package/src/ContentNegotiation.ts +10 -3
  4. package/src/Development.test.ts +119 -0
  5. package/src/Development.ts +137 -0
  6. package/src/Entity.test.ts +592 -0
  7. package/src/Entity.ts +359 -0
  8. package/src/FileRouter.ts +2 -2
  9. package/src/Http.test.ts +315 -20
  10. package/src/Http.ts +153 -11
  11. package/src/PathPattern.ts +3 -1
  12. package/src/Route.ts +26 -10
  13. package/src/RouteBody.test.ts +98 -66
  14. package/src/RouteBody.ts +125 -35
  15. package/src/RouteHook.ts +15 -14
  16. package/src/RouteHttp.test.ts +2549 -83
  17. package/src/RouteHttp.ts +337 -113
  18. package/src/RouteHttpTracer.ts +92 -0
  19. package/src/RouteMount.test.ts +23 -10
  20. package/src/RouteMount.ts +161 -4
  21. package/src/RouteSchema.test.ts +346 -0
  22. package/src/RouteSchema.ts +386 -7
  23. package/src/RouteSse.test.ts +249 -0
  24. package/src/RouteSse.ts +195 -0
  25. package/src/RouteTree.test.ts +233 -85
  26. package/src/RouteTree.ts +98 -44
  27. package/src/StreamExtra.ts +21 -1
  28. package/src/Values.test.ts +263 -0
  29. package/src/Values.ts +68 -6
  30. package/src/bun/BunBundle.ts +0 -73
  31. package/src/bun/BunHttpServer.ts +23 -7
  32. package/src/bun/BunRoute.test.ts +162 -0
  33. package/src/bun/BunRoute.ts +144 -105
  34. package/src/hyper/HyperHtml.test.ts +119 -0
  35. package/src/hyper/HyperHtml.ts +10 -2
  36. package/src/hyper/HyperNode.ts +2 -0
  37. package/src/hyper/HyperRoute.test.tsx +197 -0
  38. package/src/hyper/HyperRoute.ts +61 -0
  39. package/src/hyper/index.ts +4 -0
  40. package/src/hyper/jsx.d.ts +15 -0
  41. package/src/index.ts +2 -0
  42. package/src/node/FileSystem.ts +8 -0
  43. package/src/testing/TestLogger.test.ts +0 -3
  44. package/src/testing/TestLogger.ts +15 -9
  45. package/src/FileSystemExtra.test.ts +0 -242
  46. package/src/FileSystemExtra.ts +0 -66
@@ -1,15 +1,15 @@
1
1
  import * as test from "bun:test"
2
2
  import * as Effect from "effect/Effect"
3
+ import * as ParseResult from "effect/ParseResult"
4
+ import * as Stream from "effect/Stream"
5
+ import * as Entity from "./Entity.ts"
6
+ import * as Route from "./Route.ts"
3
7
  import * as RouteBody from "./RouteBody.ts"
4
8
  import * as RouteMount from "./RouteMount.ts"
5
9
 
6
- const text = RouteBody.build<string, "text">({
7
- format: "text",
8
- })
9
-
10
10
  test.it("infers parent descriptions", () => {
11
11
  RouteMount.get(
12
- text((ctx) =>
12
+ Route.text((ctx) =>
13
13
  Effect.gen(function*() {
14
14
  test
15
15
  .expectTypeOf(ctx)
@@ -24,13 +24,12 @@ test.it("infers parent descriptions", () => {
24
24
  )
25
25
  })
26
26
 
27
- test.it("cannot modify context", () => {
28
- text((ctx, next) =>
27
+ test.it("next is function returning Entity", () => {
28
+ Route.text((ctx, next) =>
29
29
  Effect.gen(function*() {
30
30
  test
31
31
  .expectTypeOf(next)
32
- .parameters
33
- .toEqualTypeOf<[]>()
32
+ .toExtend<() => Entity.Entity<string>>()
34
33
 
35
34
  return "Hello, world!"
36
35
  })
@@ -39,24 +38,66 @@ test.it("cannot modify context", () => {
39
38
 
40
39
  test.it("enforces result value", () => {
41
40
  // @ts-expect-error must return string
42
- text((ctx, next) =>
41
+ Route.text((ctx, next) =>
43
42
  Effect.gen(function*() {
44
43
  return 1337
45
44
  })
46
45
  )
47
46
  })
48
47
 
48
+ test.it("accepts text stream", () => {
49
+ RouteMount.get(
50
+ Route.text((ctx) =>
51
+ Effect.gen(function*() {
52
+ test
53
+ .expectTypeOf(ctx)
54
+ .toExtend<{
55
+ method: "GET"
56
+ format: "text"
57
+ }>()
58
+
59
+ return Stream.make("Hello", " ", "world!")
60
+ })
61
+ ),
62
+ )
63
+ })
64
+
65
+ test.it("accepts Effect<Stream<string>> for html format", () => {
66
+ RouteMount.get(
67
+ Route.html(function*() {
68
+ return Stream.make("<div>", "content", "</div>")
69
+ }),
70
+ )
71
+ })
72
+
73
+ test.it("accepts Effect<Stream<Uint8Array>> for bytes format", () => {
74
+ const encoder = new TextEncoder()
75
+
76
+ RouteMount.get(
77
+ Route.bytes(function*() {
78
+ return Stream.make(encoder.encode("chunk"))
79
+ }),
80
+ )
81
+ })
82
+
83
+ test.it("rejects Stream for json format", () => {
84
+ // @ts-expect-error Stream not allowed for json format
85
+ Route.json(function*() {
86
+ return Stream.make({ msg: "hello" })
87
+ })
88
+ })
89
+
49
90
  test.it("accepts value directly", () => {
50
91
  const value = "Hello, world!"
51
92
 
52
93
  test
53
- .expectTypeOf(text)
94
+ .expectTypeOf(Route.text)
54
95
  .toBeCallableWith(value)
55
96
  })
56
97
 
57
98
  test.describe(`${RouteBody.handle.name}()`, () => {
58
99
  const ctx = {}
59
- const next = () => Effect.succeed("next" as const)
100
+ const next = () => Entity.effect(Effect.succeed(Entity.make("next")))
60
101
 
61
102
  test.it("accepts all HandlerInput variants", () => {
62
103
  test
@@ -67,116 +108,107 @@ test.describe(`${RouteBody.handle.name}()`, () => {
67
108
  })
68
109
 
69
110
  test.it("handles plain value", async () => {
70
- const handler = RouteBody.handle("hello")
71
-
72
- test
73
- .expectTypeOf(handler)
74
- .returns
75
- .toEqualTypeOf<
76
- Effect.Effect<string, never, never>
77
- >()
78
-
111
+ const handler = RouteBody.handle<{}, string, never, never>("hello")
79
112
  const result = await Effect.runPromise(handler(ctx, next))
80
- test.expect(result).toBe("hello")
113
+ test.expect(result.body).toBe("hello")
114
+ test.expect(result.status).toBe(200)
81
115
  })
82
116
 
83
117
  test.it("handles Effect directly", async () => {
84
- const handler = RouteBody.handle(Effect.succeed("from effect"))
85
-
86
- test
87
- .expectTypeOf(handler)
88
- .returns
89
- .toEqualTypeOf<
90
- Effect.Effect<string, never, never>
91
- >()
92
-
118
+ const handler = RouteBody.handle<{}, string, never, never>(
119
+ Effect.succeed("from effect"),
120
+ )
93
121
  const result = await Effect.runPromise(handler(ctx, next))
94
-
95
- test
96
- .expect(result)
97
- .toBe("from effect")
122
+ test.expect(result.body).toBe("from effect")
98
123
  })
99
124
 
100
125
  test.it("handles Effect with error", async () => {
101
- const handler = RouteBody.handle(Effect.fail(new Error("oops")))
126
+ const handler = RouteBody.handle<{}, never, Error, never>(
127
+ Effect.fail(new Error("oops")),
128
+ )
102
129
 
103
130
  test
104
131
  .expectTypeOf(handler)
105
132
  .returns
106
- .toEqualTypeOf<
107
- Effect.Effect<never, Error, never>
133
+ .toExtend<
134
+ Effect.Effect<Entity.Entity<never>, Error, never>
108
135
  >()
109
136
  })
110
137
 
111
138
  test.it("handles function", async () => {
112
- const handler = RouteBody.handle(
113
- (ctx: { id: number }) => Effect.succeed(ctx.id),
139
+ const handler = RouteBody.handle<{ id: number }, number, never, never>(
140
+ (ctx) => Effect.succeed(ctx.id),
114
141
  )
115
142
 
116
143
  test
117
144
  .expectTypeOf(handler)
118
145
  .parameters
119
146
  .toEqualTypeOf<
120
- [{ id: number }, () => Effect.Effect<number>]
147
+ [
148
+ { id: number },
149
+ (
150
+ context?: Partial<{ id: number }> & Record<string, unknown>,
151
+ ) => Entity.Entity<number>,
152
+ ]
121
153
  >()
122
154
  test
123
155
  .expectTypeOf(handler)
124
156
  .returns
125
157
  .toEqualTypeOf<
126
- Effect.Effect<number, never, never>
158
+ Effect.Effect<Entity.Entity<number>, never, never>
127
159
  >()
128
160
 
129
- const result = await Effect.runPromise(
130
- handler({ id: 42 }, () => Effect.succeed(23)),
131
- )
161
+ const numNext = () => Entity.effect(Effect.succeed(Entity.make(23)))
162
+ const result = await Effect.runPromise(handler({ id: 42 }, numNext))
132
163
 
133
- test
134
- .expect(result)
135
- .toBe(42)
164
+ test.expect(result.body).toBe(42)
136
165
  })
137
166
 
138
167
  test.it("handles generator", async () => {
139
- const handler = RouteBody.handle(function*(ctx: { id: number }) {
140
- const n = yield* Effect.succeed(ctx.id)
141
- return n * 2
142
- })
168
+ const handler = RouteBody.handle<{ id: number }, number, never, never>(
169
+ function*(ctx) {
170
+ const n = yield* Effect.succeed(ctx.id)
171
+ return n * 2
172
+ },
173
+ )
143
174
 
144
175
  test
145
176
  .expectTypeOf(handler)
146
177
  .parameters
147
178
  .toEqualTypeOf<
148
- [{ id: number }, () => Effect.Effect<number>]
179
+ [
180
+ { id: number },
181
+ (
182
+ context?: Partial<{ id: number }> & Record<string, unknown>,
183
+ ) => Entity.Entity<number>,
184
+ ]
149
185
  >()
150
186
 
151
187
  test
152
188
  .expectTypeOf(handler)
153
189
  .returns
154
190
  .toEqualTypeOf<
155
- Effect.Effect<number, never, never>
191
+ Effect.Effect<Entity.Entity<number>, never, never>
156
192
  >()
157
193
 
158
- const result = await Effect.runPromise(
159
- // TODO: we should accept Effect.void in next here
160
- handler({ id: 21 }, () => Effect.succeed(23)),
161
- )
194
+ const numNext = () => Entity.effect(Effect.succeed(Entity.make(23)))
195
+ const result = await Effect.runPromise(handler({ id: 21 }, numNext))
162
196
 
163
- test
164
- .expect(result)
165
- .toBe(42)
197
+ test.expect(result.body).toBe(42)
166
198
  })
167
199
 
168
200
  test.it("generator can call next", async () => {
169
- const handler = RouteBody.handle(
170
- function*(_ctx: {}, next: () => Effect.Effect<string>) {
171
- const fromNext = yield* next()
201
+ const handler = RouteBody.handle<{}, string, ParseResult.ParseError, never>(
202
+ function*(_ctx, next) {
203
+ const fromNext = yield* next().text
172
204
  return `got: ${fromNext}`
173
205
  },
174
206
  )
175
207
 
176
- const result = await Effect.runPromise(handler(ctx, next))
208
+ const result = await Effect.runPromise(Effect.orDie(handler(ctx, next)))
177
209
 
178
210
  test
179
- .expect(result)
211
+ .expect(result.body)
180
212
  .toBe("got: next")
181
213
  })
182
214
  })
package/src/RouteBody.ts CHANGED
@@ -1,52 +1,83 @@
1
1
  import * as Effect from "effect/Effect"
2
+ import type * as Stream from "effect/Stream"
2
3
  import type * as Utils from "effect/Utils"
4
+ import * as Entity from "./Entity.ts"
3
5
  import * as Route from "./Route.ts"
6
+ import type * as Values from "./Values.ts"
4
7
 
5
8
  export type Format =
6
9
  | "text"
7
10
  | "html"
8
11
  | "json"
9
12
  | "bytes"
13
+ | "*"
14
+
15
+ const formatToContentType: Record<Format, string | undefined> = {
16
+ text: "text/plain; charset=utf-8",
17
+ html: "text/html; charset=utf-8",
18
+ json: "application/json",
19
+ bytes: "application/octet-stream",
20
+ "*": undefined,
21
+ }
22
+
23
+ type UnwrapStream<T> = T extends Stream.Stream<infer V, any, any> ? V : T
10
24
 
11
25
  export type HandlerInput<B, A, E, R> =
12
26
  | A
13
- | Effect.Effect<A, E, R>
14
- | ((context: _Simplify<B>, next: () => Effect.Effect<A>) =>
15
- | Effect.Effect<A, E, R>
16
- | Generator<Utils.YieldWrap<Effect.Effect<any, E, R>>, A, any>)
27
+ | Entity.Entity<A>
28
+ | Effect.Effect<A | Entity.Entity<A>, E, R>
29
+ | ((
30
+ context: Values.Simplify<B>,
31
+ next: (
32
+ context?: Partial<B> & Record<string, unknown>,
33
+ ) => Entity.Entity<UnwrapStream<A>>,
34
+ ) =>
35
+ | Effect.Effect<A | Entity.Entity<A>, E, R>
36
+ | Generator<
37
+ Utils.YieldWrap<Effect.Effect<unknown, E, R>>,
38
+ A | Entity.Entity<A>,
39
+ unknown
40
+ >)
17
41
 
18
- export function handle<B, A, E, R>(
19
- handler: (context: B, next: () => Effect.Effect<A>) =>
20
- | Effect.Effect<A, E, R>
21
- | Generator<Utils.YieldWrap<Effect.Effect<any, E, R>>, A, any>,
22
- ): Route.Route.HandlerImmutable<B, A, E, R>
23
- export function handle<A, E, R>(
24
- handler: Effect.Effect<A, E, R>,
25
- ): Route.Route.HandlerImmutable<{}, A, E, R>
26
- export function handle<A>(
27
- handler: A,
28
- ): Route.Route.HandlerImmutable<{}, A, never, never>
29
42
  export function handle<B, A, E, R>(
30
43
  handler: HandlerInput<B, A, E, R>,
31
- ): Route.Route.HandlerImmutable<B, A, E, R> {
44
+ ): Route.Route.Handler<B, A, E, R> {
32
45
  if (typeof handler === "function") {
33
46
  return (
34
47
  context: B,
35
- next: () => Effect.Effect<A>,
36
- ): Effect.Effect<A, E, R> => {
48
+ next: (
49
+ context?: Partial<B> & Record<string, unknown>,
50
+ ) => Entity.Entity<A>,
51
+ ): Effect.Effect<Entity.Entity<A>, E, R> => {
37
52
  const result = (handler as Function)(context, next)
38
- if (Effect.isEffect(result)) {
39
- return result as Effect.Effect<A, E, R>
40
- }
41
- return Effect.gen(function*() {
42
- return yield* result
43
- }) as Effect.Effect<A, E, R>
53
+ const effect = Effect.isEffect(result)
54
+ ? result as Effect.Effect<A | Entity.Entity<A>, E, R>
55
+ : Effect.gen(function*() {
56
+ return yield* result
57
+ }) as Effect.Effect<A | Entity.Entity<A>, E, R>
58
+ return Effect.map(effect, normalizeToEntity)
44
59
  }
45
60
  }
46
61
  if (Effect.isEffect(handler)) {
47
- return (_context, _next) => handler
62
+ return (_context, _next) =>
63
+ Effect.map(handler, normalizeToEntity) as Effect.Effect<
64
+ Entity.Entity<A>,
65
+ E,
66
+ R
67
+ >
68
+ }
69
+ if (Entity.isEntity(handler)) {
70
+ return (_context, _next) => Effect.succeed(handler as Entity.Entity<A>)
48
71
  }
49
- return (_context, _next) => Effect.succeed(handler as A)
72
+ return (_context, _next) =>
73
+ Effect.succeed(normalizeToEntity(handler as A) as Entity.Entity<A>)
74
+ }
75
+
76
+ function normalizeToEntity<A>(value: A | Entity.Entity<A>): Entity.Entity<A> {
77
+ if (Entity.isEntity(value)) {
78
+ return value as Entity.Entity<A>
79
+ }
80
+ return Entity.make(value as A, { status: 200 })
50
81
  }
51
82
 
52
83
  export function build<
@@ -59,7 +90,8 @@ export function build<
59
90
  D extends Route.RouteDescriptor.Any,
60
91
  B extends {},
61
92
  I extends Route.Route.Tuple,
62
- A extends Value,
93
+ A extends F extends "json" ? Value
94
+ : Value | Stream.Stream<Value, any, any>,
63
95
  E = never,
64
96
  R = never,
65
97
  >(
@@ -75,8 +107,28 @@ export function build<
75
107
  return function(
76
108
  self: Route.RouteSet.RouteSet<D, B, I>,
77
109
  ) {
110
+ const contentType = formatToContentType[descriptors.format]
111
+ const baseHandler = handle(handler)
112
+ const wrappedHandler: Route.Route.Handler<
113
+ D & B & Route.ExtractBindings<I> & { format: F },
114
+ A,
115
+ E,
116
+ R
117
+ > = (ctx, next) =>
118
+ Effect.map(
119
+ baseHandler(ctx as any, next as any),
120
+ (entity) =>
121
+ entity.headers["content-type"]
122
+ ? entity
123
+ : Entity.make(entity.body, {
124
+ status: entity.status,
125
+ url: entity.url,
126
+ headers: { ...entity.headers, "content-type": contentType },
127
+ }),
128
+ )
129
+
78
130
  const route = Route.make<{ format: F }, {}, A, E, R>(
79
- handle(handler) as any,
131
+ wrappedHandler as any,
80
132
  descriptors,
81
133
  )
82
134
 
@@ -97,10 +149,48 @@ export function build<
97
149
  }
98
150
  }
99
151
 
100
- // used to simplify the context type passed to route handlers
101
- // for those who prefer to write code by hand :)
102
- type _Simplify<T> = {
103
- -readonly [K in keyof T]: T[K] extends object
104
- ? { -readonly [P in keyof T[K]]: T[K][P] }
105
- : T[K]
106
- } extends infer U ? { [K in keyof U]: U[K] } : never
152
+ export type RenderValue =
153
+ | string
154
+ | Uint8Array
155
+ | Stream.Stream<string | Uint8Array, any, any>
156
+
157
+ export function render<
158
+ D extends Route.RouteDescriptor.Any,
159
+ B extends {},
160
+ I extends Route.Route.Tuple,
161
+ A extends RenderValue,
162
+ E = never,
163
+ R = never,
164
+ >(
165
+ handler: HandlerInput<
166
+ NoInfer<
167
+ D & B & Route.ExtractBindings<I> & { format: "*" }
168
+ >,
169
+ A,
170
+ E,
171
+ R
172
+ >,
173
+ ) {
174
+ return function(
175
+ self: Route.RouteSet.RouteSet<D, B, I>,
176
+ ) {
177
+ const route = Route.make<{ format: "*" }, {}, A, E, R>(
178
+ handle(handler) as any,
179
+ { format: "*" },
180
+ )
181
+
182
+ const items: [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>] = [
183
+ ...Route.items(self),
184
+ route,
185
+ ]
186
+
187
+ return Route.set<
188
+ D,
189
+ B,
190
+ [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>]
191
+ >(
192
+ items,
193
+ Route.descriptor(self),
194
+ )
195
+ }
196
+ }
package/src/RouteHook.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as Effect from "effect/Effect"
2
2
  import type * as Utils from "effect/Utils"
3
+ import * as Entity from "./Entity.ts"
3
4
  import * as Route from "./Route.ts"
4
5
 
5
6
  export type FilterResult<BOut, E, R> =
@@ -11,9 +12,9 @@ export type FilterHandlerInput<BIn, BOut, E, R> =
11
12
  | ((context: BIn) =>
12
13
  | FilterResult<BOut, E, R>
13
14
  | Generator<
14
- Utils.YieldWrap<Effect.Effect<any, E, R>>,
15
+ Utils.YieldWrap<Effect.Effect<unknown, E, R>>,
15
16
  { context: BOut },
16
- any
17
+ unknown
17
18
  >)
18
19
 
19
20
  export function filter<
@@ -34,26 +35,26 @@ export function filter<
34
35
  ): Route.RouteSet.RouteSet<
35
36
  D,
36
37
  SB,
37
- [...P, Route.Route.Route<{}, BOut, void, E, R>]
38
+ [
39
+ ...P,
40
+ Route.Route.Route<{}, BOut, unknown, E, R>,
41
+ ]
38
42
  > {
39
43
  const route = Route.make<
40
44
  {},
41
45
  BOut,
42
- void,
46
+ unknown,
43
47
  E,
44
48
  R
45
- >((context: BOut, next) =>
49
+ >((context: BOut, next: (ctx?: Partial<BOut>) => Entity.Entity<unknown>) =>
46
50
  Effect.gen(function*() {
47
51
  const filterResult = yield* normalized(context as unknown as BIn)
48
52
 
49
- yield* next(
50
- filterResult
51
- ? {
52
- ...context,
53
- ...filterResult.context,
54
- }
55
- : context,
56
- )
53
+ const mergedContext = filterResult
54
+ ? { ...context, ...filterResult.context }
55
+ : context
56
+
57
+ return yield* Entity.resolve(next(mergedContext as Partial<BOut>))
57
58
  })
58
59
  )
59
60
 
@@ -61,7 +62,7 @@ export function filter<
61
62
  [
62
63
  ...Route.items(self),
63
64
  route,
64
- ] as [...P, Route.Route.Route<{}, BOut, void, E, R>],
65
+ ] as [...P, Route.Route.Route<{}, BOut, unknown, E, R>],
65
66
  Route.descriptor(self),
66
67
  )
67
68
  }