effect-start 0.17.0 → 0.17.2
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/dist/Commander.d.ts +103 -0
- package/dist/Commander.js +333 -0
- package/dist/ContentNegotiation.d.ts +13 -0
- package/dist/ContentNegotiation.js +364 -0
- package/dist/Development.d.ts +34 -0
- package/dist/Development.js +52 -0
- package/dist/Entity.d.ts +47 -0
- package/dist/Entity.js +224 -0
- package/dist/FileRouter.d.ts +61 -0
- package/dist/FileRouter.js +203 -0
- package/dist/FileRouterCodegen.d.ts +19 -0
- package/dist/FileRouterCodegen.js +176 -0
- package/dist/FileRouterPattern.d.ts +9 -0
- package/dist/FileRouterPattern.js +35 -0
- package/dist/Http.d.ts +37 -0
- package/dist/Http.js +92 -0
- package/dist/HttpAppExtra.d.ts +7 -0
- package/dist/HttpAppExtra.js +320 -0
- package/dist/HttpUtils.d.ts +3 -0
- package/dist/HttpUtils.js +11 -0
- package/dist/PathPattern.d.ts +134 -0
- package/dist/PathPattern.js +415 -0
- package/dist/Random.d.ts +5 -0
- package/dist/Random.js +49 -0
- package/dist/Route.d.ts +98 -0
- package/dist/Route.js +81 -0
- package/dist/RouteBody.d.ts +53 -0
- package/dist/RouteBody.js +67 -0
- package/dist/RouteHook.d.ts +12 -0
- package/dist/RouteHook.js +45 -0
- package/dist/RouteHttp.d.ts +21 -0
- package/dist/RouteHttp.js +260 -0
- package/dist/RouteHttpTracer.d.ts +10 -0
- package/dist/RouteHttpTracer.js +62 -0
- package/dist/RouteMount.d.ts +119 -0
- package/dist/RouteMount.js +77 -0
- package/dist/RouteSchema.d.ts +65 -0
- package/dist/RouteSchema.js +155 -0
- package/dist/RouteSse.d.ts +21 -0
- package/dist/RouteSse.js +85 -0
- package/dist/RouteTree.d.ts +56 -0
- package/dist/RouteTree.js +91 -0
- package/dist/RouteTrie.d.ts +20 -0
- package/dist/RouteTrie.js +157 -0
- package/dist/RouterPattern.d.ts +118 -0
- package/dist/RouterPattern.js +269 -0
- package/dist/SchemaExtra.d.ts +7 -0
- package/dist/SchemaExtra.js +74 -0
- package/dist/Start.d.ts +19 -0
- package/dist/Start.js +23 -0
- package/dist/StartApp.d.ts +19 -0
- package/dist/StartApp.js +21 -0
- package/dist/StreamExtra.d.ts +28 -0
- package/dist/StreamExtra.js +100 -0
- package/dist/TuplePathPattern.d.ts +9 -0
- package/dist/TuplePathPattern.js +63 -0
- package/dist/Values.d.ts +26 -0
- package/dist/Values.js +30 -0
- package/dist/bun/BunBundle.d.ts +12 -0
- package/dist/bun/BunBundle.js +145 -0
- package/dist/bun/BunHttpServer.d.ts +44 -0
- package/dist/bun/BunHttpServer.js +187 -0
- package/dist/bun/BunHttpServer_web.d.ts +60 -0
- package/dist/bun/BunHttpServer_web.js +252 -0
- package/dist/bun/BunImportTrackerPlugin.d.ts +13 -0
- package/dist/bun/BunImportTrackerPlugin.js +71 -0
- package/dist/bun/BunRoute.d.ts +49 -0
- package/dist/bun/BunRoute.js +131 -0
- package/dist/bun/BunRuntime.d.ts +1 -0
- package/dist/bun/BunRuntime.js +26 -0
- package/dist/bun/BunVirtualFilesPlugin.d.ts +4 -0
- package/dist/bun/BunVirtualFilesPlugin.js +40 -0
- package/dist/bun/_BunEnhancedResolve.d.ts +45 -0
- package/dist/bun/_BunEnhancedResolve.js +104 -0
- package/dist/bun/index.d.ts +4 -0
- package/dist/bun/index.js +4 -0
- package/dist/bundler/Bundle.d.ts +60 -0
- package/dist/bundler/Bundle.js +48 -0
- package/dist/bundler/BundleFiles.d.ts +13 -0
- package/dist/bundler/BundleFiles.js +94 -0
- package/dist/bundler/BundleHttp.d.ts +45 -0
- package/dist/bundler/BundleHttp.js +176 -0
- package/dist/client/Overlay.d.ts +2 -0
- package/dist/client/Overlay.js +32 -0
- package/dist/client/ScrollState.d.ts +6 -0
- package/dist/client/ScrollState.js +98 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +81 -0
- package/dist/experimental/EncryptedCookies.d.ts +51 -0
- package/dist/experimental/EncryptedCookies.js +243 -0
- package/dist/experimental/SseHttpResponse.d.ts +7 -0
- package/dist/experimental/SseHttpResponse.js +28 -0
- package/dist/experimental/index.d.ts +2 -0
- package/dist/experimental/index.js +2 -0
- package/dist/hyper/Hyper.d.ts +32 -0
- package/dist/hyper/Hyper.js +34 -0
- package/dist/hyper/HyperHtml.d.ts +23 -0
- package/dist/hyper/HyperHtml.js +144 -0
- package/dist/hyper/HyperNode.d.ts +14 -0
- package/dist/hyper/HyperNode.js +11 -0
- package/dist/hyper/HyperRoute.d.ts +8 -0
- package/dist/hyper/HyperRoute.js +32 -0
- package/dist/hyper/HyperRoute.test.d.ts +1 -0
- package/dist/hyper/HyperRoute.test.js +72 -0
- package/dist/hyper/index.d.ts +4 -0
- package/dist/hyper/index.js +4 -0
- package/dist/hyper/jsx-runtime.d.ts +7 -0
- package/dist/hyper/jsx-runtime.js +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/inference_check.d.ts +1 -0
- package/dist/inference_check.js +15 -0
- package/dist/middlewares/BasicAuthMiddleware.d.ts +8 -0
- package/dist/middlewares/BasicAuthMiddleware.js +22 -0
- package/dist/middlewares/index.d.ts +1 -0
- package/dist/middlewares/index.js +1 -0
- package/dist/node/FileSystem.d.ts +9 -0
- package/dist/node/FileSystem.js +440 -0
- package/dist/node/Utils.d.ts +1 -0
- package/dist/node/Utils.js +19 -0
- package/dist/repro_fail.d.ts +1 -0
- package/dist/repro_fail.js +14 -0
- package/dist/testing/TestHttpClient.d.ts +13 -0
- package/dist/testing/TestHttpClient.js +68 -0
- package/dist/testing/TestLogger.d.ts +13 -0
- package/dist/testing/TestLogger.js +29 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/utils.d.ts +9 -0
- package/dist/testing/utils.js +39 -0
- package/dist/x/cloudflare/CloudflareTunnel.d.ts +13 -0
- package/dist/x/cloudflare/CloudflareTunnel.js +43 -0
- package/dist/x/cloudflare/index.d.ts +1 -0
- package/dist/x/cloudflare/index.js +1 -0
- package/dist/x/datastar/Datastar.d.ts +6 -0
- package/dist/x/datastar/Datastar.js +46 -0
- package/dist/x/datastar/index.d.ts +2 -0
- package/dist/x/datastar/index.js +2 -0
- package/dist/x/tailwind/TailwindPlugin.d.ts +23 -0
- package/dist/x/tailwind/TailwindPlugin.js +219 -0
- package/dist/x/tailwind/compile.d.ts +19 -0
- package/dist/x/tailwind/compile.js +156 -0
- package/dist/x/tailwind/plugin.d.ts +2 -0
- package/dist/x/tailwind/plugin.js +15 -0
- package/package.json +68 -16
- package/src/RouteBody.test.ts +18 -0
- package/src/RouteBody.ts +126 -2
- package/src/x/tailwind/compile.ts +8 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as Function from "effect/Function";
|
|
2
|
+
import * as Route from "./Route.js";
|
|
3
|
+
const RouteSetTypeId = Symbol.for("effect-start/RouteSet");
|
|
4
|
+
export const use = makeMethodDescriber("*");
|
|
5
|
+
export const get = makeMethodDescriber("GET");
|
|
6
|
+
export const post = makeMethodDescriber("POST");
|
|
7
|
+
export const put = makeMethodDescriber("PUT");
|
|
8
|
+
export const del = makeMethodDescriber("DELETE");
|
|
9
|
+
export const patch = makeMethodDescriber("PATCH");
|
|
10
|
+
export const head = makeMethodDescriber("HEAD");
|
|
11
|
+
export const options = makeMethodDescriber("OPTIONS");
|
|
12
|
+
export const add = function (path, routes) {
|
|
13
|
+
const baseItems = Route.isRouteSet(this)
|
|
14
|
+
? Route.items(this)
|
|
15
|
+
: [];
|
|
16
|
+
const routeSet = typeof routes === "function"
|
|
17
|
+
? routes(make([]))
|
|
18
|
+
: routes;
|
|
19
|
+
const routeItems = Route.items(routeSet);
|
|
20
|
+
const newItems = routeItems.map((item) => {
|
|
21
|
+
const itemDescriptor = Route.descriptor(item);
|
|
22
|
+
const concatenatedPath = typeof itemDescriptor?.path === "string"
|
|
23
|
+
? path + itemDescriptor.path
|
|
24
|
+
: path;
|
|
25
|
+
const newDescriptor = { ...itemDescriptor, path: concatenatedPath };
|
|
26
|
+
return Route.isRoute(item)
|
|
27
|
+
? Route.make(item.handler, newDescriptor)
|
|
28
|
+
: Route.set(Route.items(item), newDescriptor);
|
|
29
|
+
});
|
|
30
|
+
return make([
|
|
31
|
+
...baseItems,
|
|
32
|
+
...newItems,
|
|
33
|
+
]);
|
|
34
|
+
};
|
|
35
|
+
const Proto = Object.assign(Object.create(null), {
|
|
36
|
+
[RouteSetTypeId]: RouteSetTypeId,
|
|
37
|
+
*[Symbol.iterator]() {
|
|
38
|
+
yield* Route.items(this);
|
|
39
|
+
},
|
|
40
|
+
use,
|
|
41
|
+
get,
|
|
42
|
+
post,
|
|
43
|
+
put,
|
|
44
|
+
del,
|
|
45
|
+
patch,
|
|
46
|
+
head,
|
|
47
|
+
options,
|
|
48
|
+
add,
|
|
49
|
+
});
|
|
50
|
+
function make(items) {
|
|
51
|
+
return Object.assign(Object.create(Proto), {
|
|
52
|
+
[Route.RouteItems]: items,
|
|
53
|
+
[Route.RouteDescriptor]: {},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function makeMethodDescriber(method) {
|
|
57
|
+
function describeMethod(...fs) {
|
|
58
|
+
const baseItems = Route.isRouteSet(this)
|
|
59
|
+
? Route.items(this)
|
|
60
|
+
: [];
|
|
61
|
+
const methodSet = Route.set([], { method });
|
|
62
|
+
const f = Function.flow(...fs);
|
|
63
|
+
const result = f(methodSet);
|
|
64
|
+
const resultItems = Route.items(result);
|
|
65
|
+
// Items are already flat (only Routes), just merge method into each descriptor
|
|
66
|
+
const flattenedItems = resultItems.map((item) => {
|
|
67
|
+
const itemDescriptor = Route.descriptor(item);
|
|
68
|
+
const newDescriptor = { method, ...itemDescriptor };
|
|
69
|
+
return Route.make(item.handler, newDescriptor);
|
|
70
|
+
});
|
|
71
|
+
return make([
|
|
72
|
+
...baseItems,
|
|
73
|
+
...flattenedItems,
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
return describeMethod;
|
|
77
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as ParseResult from "effect/ParseResult";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import type * as Scope from "effect/Scope";
|
|
4
|
+
import * as Http from "./Http.ts";
|
|
5
|
+
import * as Route from "./Route.ts";
|
|
6
|
+
export interface RequestBodyError {
|
|
7
|
+
readonly _tag: "RequestBodyError";
|
|
8
|
+
readonly reason: "JsonError" | "UrlParamsError" | "MultipartError" | "FormDataError";
|
|
9
|
+
readonly cause: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare const RequestBodyError: (reason: RequestBodyError["reason"], cause: unknown) => RequestBodyError;
|
|
12
|
+
export declare const File: Schema.TaggedStruct<"File", {
|
|
13
|
+
key: typeof Schema.String;
|
|
14
|
+
name: typeof Schema.String;
|
|
15
|
+
contentType: typeof Schema.String;
|
|
16
|
+
content: typeof Schema.Uint8ArrayFromSelf;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function schemaHeaders<A, I extends Readonly<Record<string, string | undefined>>, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
19
|
+
...P,
|
|
20
|
+
Route.Route.Route<{}, {
|
|
21
|
+
headers: A;
|
|
22
|
+
}, unknown, ParseResult.ParseError, R>
|
|
23
|
+
]>;
|
|
24
|
+
export declare function schemaCookies<A, I extends Readonly<Record<string, string | undefined>>, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
25
|
+
...P,
|
|
26
|
+
Route.Route.Route<{}, {
|
|
27
|
+
cookies: A;
|
|
28
|
+
}, unknown, ParseResult.ParseError, R>
|
|
29
|
+
]>;
|
|
30
|
+
export declare function schemaSearchParams<A, I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
31
|
+
...P,
|
|
32
|
+
Route.Route.Route<{}, {
|
|
33
|
+
searchParams: A;
|
|
34
|
+
}, unknown, ParseResult.ParseError, R>
|
|
35
|
+
]>;
|
|
36
|
+
export declare function schemaPathParams<A, I extends Readonly<Record<string, string | undefined>>, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
37
|
+
...P,
|
|
38
|
+
Route.Route.Route<{}, {
|
|
39
|
+
pathParams: A;
|
|
40
|
+
}, unknown, ParseResult.ParseError, R>
|
|
41
|
+
]>;
|
|
42
|
+
export declare function schemaBodyJson<A, I, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
43
|
+
...P,
|
|
44
|
+
Route.Route.Route<{}, {
|
|
45
|
+
body: A;
|
|
46
|
+
}, unknown, RequestBodyError | ParseResult.ParseError, R>
|
|
47
|
+
]>;
|
|
48
|
+
export declare function schemaBodyUrlParams<A, I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
49
|
+
...P,
|
|
50
|
+
Route.Route.Route<{}, {
|
|
51
|
+
body: A;
|
|
52
|
+
}, unknown, RequestBodyError | ParseResult.ParseError, R>
|
|
53
|
+
]>;
|
|
54
|
+
export declare function schemaBodyMultipart<A, I extends Partial<Record<string, ReadonlyArray<Http.FilePart> | ReadonlyArray<string> | string>>, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
55
|
+
...P,
|
|
56
|
+
Route.Route.Route<{}, {
|
|
57
|
+
body: A;
|
|
58
|
+
}, unknown, RequestBodyError | ParseResult.ParseError, R | Scope.Scope>
|
|
59
|
+
]>;
|
|
60
|
+
export declare function schemaBodyForm<A, I extends Partial<Record<string, ReadonlyArray<Http.FilePart> | ReadonlyArray<string> | string>>, R>(fields: Schema.Schema<A, I, R>): <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(self: Route.RouteSet.RouteSet<D, SB, P>) => Route.RouteSet.RouteSet<D, SB, [
|
|
61
|
+
...P,
|
|
62
|
+
Route.Route.Route<{}, {
|
|
63
|
+
body: A;
|
|
64
|
+
}, unknown, RequestBodyError | ParseResult.ParseError, R | Scope.Scope>
|
|
65
|
+
]>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as Http from "./Http.js";
|
|
4
|
+
import * as PathPattern from "./PathPattern.js";
|
|
5
|
+
import * as RouteHook from "./RouteHook.js";
|
|
6
|
+
export const RequestBodyError = (reason, cause) => ({ _tag: "RequestBodyError", reason, cause });
|
|
7
|
+
export const File = Schema.TaggedStruct("File", {
|
|
8
|
+
key: Schema.String,
|
|
9
|
+
name: Schema.String,
|
|
10
|
+
contentType: Schema.String,
|
|
11
|
+
content: Schema.Uint8ArrayFromSelf,
|
|
12
|
+
});
|
|
13
|
+
export function schemaHeaders(fields) {
|
|
14
|
+
const decode = Schema.decodeUnknown(fields);
|
|
15
|
+
return RouteHook.filter((ctx) => Effect.map(decode(Http.mapHeaders(ctx.request.headers)), (parsed) => ({
|
|
16
|
+
context: {
|
|
17
|
+
headers: {
|
|
18
|
+
...ctx.headers,
|
|
19
|
+
...parsed,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
24
|
+
export function schemaCookies(fields) {
|
|
25
|
+
const decode = Schema.decodeUnknown(fields);
|
|
26
|
+
return RouteHook.filter((ctx) => Effect.map(decode(Http.parseCookies(ctx.request.headers.get("cookie"))), (parsed) => ({
|
|
27
|
+
context: {
|
|
28
|
+
cookies: {
|
|
29
|
+
...ctx.cookies,
|
|
30
|
+
...parsed,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
})));
|
|
34
|
+
}
|
|
35
|
+
export function schemaSearchParams(fields) {
|
|
36
|
+
const decode = Schema.decodeUnknown(fields);
|
|
37
|
+
return RouteHook.filter((ctx) => {
|
|
38
|
+
const url = new URL(ctx.request.url);
|
|
39
|
+
return Effect.map(decode(Http.mapUrlSearchParams(url.searchParams)), (parsed) => ({
|
|
40
|
+
context: {
|
|
41
|
+
searchParams: {
|
|
42
|
+
...ctx.searchParams,
|
|
43
|
+
...parsed,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function schemaPathParams(fields) {
|
|
50
|
+
const decode = Schema.decodeUnknown(fields);
|
|
51
|
+
return RouteHook.filter((ctx) => {
|
|
52
|
+
const url = new URL(ctx.request.url);
|
|
53
|
+
const pattern = ctx.path ?? "/";
|
|
54
|
+
const params = PathPattern.match(pattern, url.pathname) ?? {};
|
|
55
|
+
return Effect.map(decode(params), (parsed) => ({
|
|
56
|
+
context: {
|
|
57
|
+
pathParams: {
|
|
58
|
+
...ctx.pathParams,
|
|
59
|
+
...parsed,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function schemaBodyJson(fields) {
|
|
66
|
+
const decode = Schema.decodeUnknown(fields);
|
|
67
|
+
return RouteHook.filter((ctx) => Effect.gen(function* () {
|
|
68
|
+
const json = yield* Effect.tryPromise({
|
|
69
|
+
try: () => ctx.request.json(),
|
|
70
|
+
catch: (error) => RequestBodyError("JsonError", error),
|
|
71
|
+
});
|
|
72
|
+
const parsed = yield* decode(json);
|
|
73
|
+
return {
|
|
74
|
+
context: {
|
|
75
|
+
body: {
|
|
76
|
+
...ctx.body,
|
|
77
|
+
...parsed,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
export function schemaBodyUrlParams(fields) {
|
|
84
|
+
const decode = Schema.decodeUnknown(fields);
|
|
85
|
+
return RouteHook.filter((ctx) => Effect.gen(function* () {
|
|
86
|
+
const text = yield* Effect.tryPromise({
|
|
87
|
+
try: () => ctx.request.text(),
|
|
88
|
+
catch: (error) => RequestBodyError("UrlParamsError", error),
|
|
89
|
+
});
|
|
90
|
+
const params = new URLSearchParams(text);
|
|
91
|
+
const parsed = yield* decode(Http.mapUrlSearchParams(params));
|
|
92
|
+
return {
|
|
93
|
+
context: {
|
|
94
|
+
body: {
|
|
95
|
+
...ctx.body,
|
|
96
|
+
...parsed,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
export function schemaBodyMultipart(fields) {
|
|
103
|
+
const decode = Schema.decodeUnknown(fields);
|
|
104
|
+
return RouteHook.filter((ctx) => Effect.gen(function* () {
|
|
105
|
+
const record = yield* Effect.tryPromise({
|
|
106
|
+
try: () => Http.parseFormData(ctx.request),
|
|
107
|
+
catch: (error) => RequestBodyError("MultipartError", error),
|
|
108
|
+
});
|
|
109
|
+
const parsed = yield* decode(record);
|
|
110
|
+
return {
|
|
111
|
+
context: {
|
|
112
|
+
body: {
|
|
113
|
+
...ctx.body,
|
|
114
|
+
...parsed,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
export function schemaBodyForm(fields) {
|
|
121
|
+
const decode = Schema.decodeUnknown(fields);
|
|
122
|
+
return RouteHook.filter((ctx) => Effect.gen(function* () {
|
|
123
|
+
const contentType = ctx.request.headers.get("content-type") ?? "";
|
|
124
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
125
|
+
const text = yield* Effect.tryPromise({
|
|
126
|
+
try: () => ctx.request.text(),
|
|
127
|
+
catch: (error) => RequestBodyError("UrlParamsError", error),
|
|
128
|
+
});
|
|
129
|
+
const params = new URLSearchParams(text);
|
|
130
|
+
const record = Http.mapUrlSearchParams(params);
|
|
131
|
+
const parsed = yield* decode(record);
|
|
132
|
+
return {
|
|
133
|
+
context: {
|
|
134
|
+
body: {
|
|
135
|
+
...ctx.body,
|
|
136
|
+
...parsed,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const record = yield* Effect.tryPromise({
|
|
142
|
+
try: () => Http.parseFormData(ctx.request),
|
|
143
|
+
catch: (error) => RequestBodyError("FormDataError", error),
|
|
144
|
+
});
|
|
145
|
+
const parsed = yield* decode(record);
|
|
146
|
+
return {
|
|
147
|
+
context: {
|
|
148
|
+
body: {
|
|
149
|
+
...ctx.body,
|
|
150
|
+
...parsed,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Stream from "effect/Stream";
|
|
3
|
+
import type * as Utils from "effect/Utils";
|
|
4
|
+
import * as Entity from "./Entity.ts";
|
|
5
|
+
import * as Route from "./Route.ts";
|
|
6
|
+
import type * as Values from "./Values.ts";
|
|
7
|
+
export interface SseEvent {
|
|
8
|
+
data?: string | undefined;
|
|
9
|
+
event?: string;
|
|
10
|
+
retry?: number;
|
|
11
|
+
}
|
|
12
|
+
export type SseTaggedEvent = {
|
|
13
|
+
readonly _tag: string;
|
|
14
|
+
};
|
|
15
|
+
export type SseEventInput = SseEvent | SseTaggedEvent;
|
|
16
|
+
export type SseHandlerInput<B, E, R> = Stream.Stream<SseEventInput, E, R> | Effect.Effect<Stream.Stream<SseEventInput, E, R>, E, R> | ((context: Values.Simplify<B>, next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<string>) => Stream.Stream<SseEventInput, E, R> | Effect.Effect<Stream.Stream<SseEventInput, E, R>, E, R> | Generator<Utils.YieldWrap<Effect.Effect<unknown, E, R>>, Stream.Stream<SseEventInput, E, R>, unknown>);
|
|
17
|
+
export declare function sse<D extends Route.RouteDescriptor.Any, B extends {}, I extends Route.Route.Tuple, E = never, R = never>(handler: SseHandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & {
|
|
18
|
+
format: "text";
|
|
19
|
+
}>, E, R>): (self: Route.RouteSet.RouteSet<D, B, I>) => Route.RouteSet.RouteSet<D, B, [...I, Route.Route.Route<{
|
|
20
|
+
format: "text";
|
|
21
|
+
}, {}, Stream.Stream<string, E, R>, E, R>]>;
|
package/dist/RouteSse.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schedule from "effect/Schedule";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import * as Entity from "./Entity.js";
|
|
6
|
+
import * as Route from "./Route.js";
|
|
7
|
+
import * as StreamExtra from "./StreamExtra.js";
|
|
8
|
+
const HEARTBEAT_INTERVAL = Duration.seconds(5);
|
|
9
|
+
const HEARTBEAT = ": <3\n\n";
|
|
10
|
+
function isTaggedEvent(event) {
|
|
11
|
+
return Object.hasOwn(event, "_tag")
|
|
12
|
+
&& typeof event["_tag"] === "string";
|
|
13
|
+
}
|
|
14
|
+
function formatSseEvent(event) {
|
|
15
|
+
if (isTaggedEvent(event)) {
|
|
16
|
+
const json = JSON.stringify(event);
|
|
17
|
+
return `event: ${event._tag}\ndata: ${json}\n\n`;
|
|
18
|
+
}
|
|
19
|
+
const e = event;
|
|
20
|
+
let result = "";
|
|
21
|
+
if (e.event) {
|
|
22
|
+
result += `event: ${e.event}\n`;
|
|
23
|
+
}
|
|
24
|
+
if (typeof e.data === "string") {
|
|
25
|
+
for (const line of e.data.split("\n")) {
|
|
26
|
+
result += `data: ${line}\n`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (e.retry !== undefined) {
|
|
30
|
+
result += `retry: ${e.retry}\n`;
|
|
31
|
+
}
|
|
32
|
+
if (result === "") {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
return result + "\n";
|
|
36
|
+
}
|
|
37
|
+
export function sse(handler) {
|
|
38
|
+
return function (self) {
|
|
39
|
+
const sseHandler = (ctx, _next) => {
|
|
40
|
+
const getStream = () => {
|
|
41
|
+
if (typeof handler === "function") {
|
|
42
|
+
const result = handler(ctx, _next);
|
|
43
|
+
if (StreamExtra.isStream(result)) {
|
|
44
|
+
return Effect.succeed(result);
|
|
45
|
+
}
|
|
46
|
+
if (Effect.isEffect(result)) {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
return Effect.gen(function* () {
|
|
50
|
+
return yield* result;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (StreamExtra.isStream(handler)) {
|
|
54
|
+
return Effect.succeed(handler);
|
|
55
|
+
}
|
|
56
|
+
if (Effect.isEffect(handler)) {
|
|
57
|
+
return handler;
|
|
58
|
+
}
|
|
59
|
+
return Effect.succeed(Stream.empty);
|
|
60
|
+
};
|
|
61
|
+
return Effect.map(getStream(), (eventStream) => {
|
|
62
|
+
const formattedStream = Stream.map(eventStream, formatSseEvent);
|
|
63
|
+
const heartbeat = Stream
|
|
64
|
+
.repeat(Stream.succeed(HEARTBEAT), Schedule.spaced(HEARTBEAT_INTERVAL))
|
|
65
|
+
.pipe(Stream.drop(1));
|
|
66
|
+
const merged = Stream.merge(formattedStream, heartbeat, {
|
|
67
|
+
haltStrategy: "left",
|
|
68
|
+
});
|
|
69
|
+
return Entity.make(merged, {
|
|
70
|
+
headers: {
|
|
71
|
+
"content-type": "text/event-stream",
|
|
72
|
+
"cache-control": "no-cache",
|
|
73
|
+
"connection": "keep-alive",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const route = Route.make(sseHandler, { format: "text" });
|
|
79
|
+
const items = [
|
|
80
|
+
...Route.items(self),
|
|
81
|
+
route,
|
|
82
|
+
];
|
|
83
|
+
return Route.set(items, Route.descriptor(self));
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as PathPattern from "./PathPattern.ts";
|
|
2
|
+
import * as Route from "./Route.ts";
|
|
3
|
+
import * as RouteMount from "./RouteMount.ts";
|
|
4
|
+
declare const TypeId: unique symbol;
|
|
5
|
+
declare const RouteTreeRoutes: unique symbol;
|
|
6
|
+
type MethodRoute = Route.Route.With<{
|
|
7
|
+
method: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type RouteTuple = Iterable<MethodRoute>;
|
|
10
|
+
export type LayerRoute = Iterable<Route.Route.With<{
|
|
11
|
+
method: "*";
|
|
12
|
+
}>>;
|
|
13
|
+
type LayerKey = "*";
|
|
14
|
+
declare const LayerKey: LayerKey;
|
|
15
|
+
export type InputRouteMap = {
|
|
16
|
+
[LayerKey]?: LayerRoute;
|
|
17
|
+
} & {
|
|
18
|
+
[path: PathPattern.PathPattern]: RouteTuple | RouteTree;
|
|
19
|
+
};
|
|
20
|
+
export type RouteMap = {
|
|
21
|
+
[path: PathPattern.PathPattern]: Route.Route.Tuple;
|
|
22
|
+
};
|
|
23
|
+
export type Routes<T extends RouteTree> = T[typeof RouteTreeRoutes];
|
|
24
|
+
export interface RouteTree<Routes extends RouteMap = RouteMap> {
|
|
25
|
+
[TypeId]: typeof TypeId;
|
|
26
|
+
[RouteTreeRoutes]: Routes;
|
|
27
|
+
}
|
|
28
|
+
type PrefixKeys<T, Prefix extends string> = {
|
|
29
|
+
[K in keyof T as K extends string ? `${Prefix}${K}` : never]: T[K];
|
|
30
|
+
};
|
|
31
|
+
type InferItems<T> = T extends Route.RouteSet.Data<any, any, infer M> ? M : [];
|
|
32
|
+
type LayerItems<T extends InputRouteMap> = "*" extends keyof T ? InferItems<T["*"]> : [];
|
|
33
|
+
type FlattenRouteMap<T extends InputRouteMap> = {
|
|
34
|
+
[K in Exclude<keyof T, "*"> as T[K] extends RouteTree ? never : K]: [
|
|
35
|
+
...LayerItems<T>,
|
|
36
|
+
...InferItems<T[K]>
|
|
37
|
+
];
|
|
38
|
+
} & UnionToIntersection<FlattenNested<T, Exclude<keyof T, "*">, LayerItems<T>>>;
|
|
39
|
+
type FlattenNested<T, K, L extends Route.Route.Tuple> = K extends keyof T ? T[K] extends RouteTree<infer R> ? PrefixKeys<PrependLayers<R, L>, K & string> : {} : {};
|
|
40
|
+
type PrependLayers<T extends RouteMap, L extends Route.Route.Tuple> = {
|
|
41
|
+
[K in keyof T]: T[K] extends Route.Route.Tuple ? [...L, ...T[K]] : never;
|
|
42
|
+
};
|
|
43
|
+
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
44
|
+
export declare function make<const Routes extends InputRouteMap>(input: Routes): RouteTree<FlattenRouteMap<Routes>>;
|
|
45
|
+
export type WalkDescriptor = {
|
|
46
|
+
path: PathPattern.PathPattern;
|
|
47
|
+
method: string;
|
|
48
|
+
} & Route.RouteDescriptor.Any;
|
|
49
|
+
export declare function walk(tree: RouteTree): Generator<RouteMount.MountedRoute>;
|
|
50
|
+
export declare function isRouteTree(input: unknown): input is RouteTree;
|
|
51
|
+
export interface LookupResult {
|
|
52
|
+
route: RouteMount.MountedRoute;
|
|
53
|
+
params: Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
export declare function lookup(tree: RouteTree, method: string, path: string): LookupResult | null;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as Predicate from "effect/Predicate";
|
|
2
|
+
import * as PathPattern from "./PathPattern.js";
|
|
3
|
+
import * as Route from "./Route.js";
|
|
4
|
+
const TypeId = Symbol.for("effect-start/RouteTree");
|
|
5
|
+
const RouteTreeRoutes = Symbol();
|
|
6
|
+
const LayerKey = "*";
|
|
7
|
+
function routes(tree) {
|
|
8
|
+
return tree[RouteTreeRoutes];
|
|
9
|
+
}
|
|
10
|
+
// segment priority: static (0) < :param (1) < :param? (2) < :param+ (3) < :param* (4)
|
|
11
|
+
function sortScore(path) {
|
|
12
|
+
const segments = path.split("/");
|
|
13
|
+
const greedyIdx = segments.findIndex((s) => s.endsWith("*") || s.endsWith("+"));
|
|
14
|
+
const maxPriority = Math.max(...segments.map((s) => !s.startsWith(":")
|
|
15
|
+
? 0
|
|
16
|
+
: s.endsWith("*")
|
|
17
|
+
? 4
|
|
18
|
+
: s.endsWith("+")
|
|
19
|
+
? 3
|
|
20
|
+
: s.endsWith("?")
|
|
21
|
+
? 2
|
|
22
|
+
: 1), 0);
|
|
23
|
+
return greedyIdx === -1
|
|
24
|
+
// non-greedy: sort by depth, then by max segment priority
|
|
25
|
+
? (segments.length << 16) + (maxPriority << 8)
|
|
26
|
+
// greedy: sort after non-greedy, by greedy position (later = first), then priority
|
|
27
|
+
: (1 << 24) + ((16 - greedyIdx) << 16) + (maxPriority << 8);
|
|
28
|
+
}
|
|
29
|
+
function sortRoutes(input) {
|
|
30
|
+
const keys = Object.keys(input).sort((a, b) => sortScore(a) - sortScore(b) || a.localeCompare(b));
|
|
31
|
+
const sorted = {};
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
sorted[key] =
|
|
34
|
+
input[key];
|
|
35
|
+
}
|
|
36
|
+
return sorted;
|
|
37
|
+
}
|
|
38
|
+
export function make(input) {
|
|
39
|
+
const layerRoutes = [...(input[LayerKey] ?? [])];
|
|
40
|
+
const merged = {};
|
|
41
|
+
function flatten(map, prefix, layers) {
|
|
42
|
+
for (const key of Object.keys(map)) {
|
|
43
|
+
if (key === LayerKey)
|
|
44
|
+
continue;
|
|
45
|
+
const path = key;
|
|
46
|
+
const entry = map[path];
|
|
47
|
+
const fullPath = `${prefix}${path}`;
|
|
48
|
+
if (isRouteTree(entry)) {
|
|
49
|
+
flatten(routes(entry), fullPath, layers);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
merged[fullPath] = [...layers, ...entry];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
flatten(input, "", layerRoutes);
|
|
57
|
+
return {
|
|
58
|
+
[TypeId]: TypeId,
|
|
59
|
+
[RouteTreeRoutes]: sortRoutes(merged),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function* flattenRoutes(path, routes) {
|
|
63
|
+
for (const route of routes) {
|
|
64
|
+
const descriptor = {
|
|
65
|
+
...route[Route.RouteDescriptor],
|
|
66
|
+
path,
|
|
67
|
+
};
|
|
68
|
+
yield Route.make(route.handler, descriptor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function* walk(tree) {
|
|
72
|
+
const _routes = routes(tree);
|
|
73
|
+
for (const path of Object.keys(_routes)) {
|
|
74
|
+
yield* flattenRoutes(path, _routes[path]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function isRouteTree(input) {
|
|
78
|
+
return Predicate.hasProperty(input, TypeId);
|
|
79
|
+
}
|
|
80
|
+
export function lookup(tree, method, path) {
|
|
81
|
+
for (const route of walk(tree)) {
|
|
82
|
+
const descriptor = Route.descriptor(route);
|
|
83
|
+
if (descriptor.method !== "*" && descriptor.method !== method)
|
|
84
|
+
continue;
|
|
85
|
+
const params = PathPattern.match(descriptor.path, path);
|
|
86
|
+
if (params !== null) {
|
|
87
|
+
return { route, params };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Route from "./Route.ts";
|
|
2
|
+
export interface Node {
|
|
3
|
+
children: Record<string, Node>;
|
|
4
|
+
paramChild: Node | null;
|
|
5
|
+
paramName: string | null;
|
|
6
|
+
requiredWildcardChild: Node | null;
|
|
7
|
+
requiredWildcardName: string | null;
|
|
8
|
+
optionalWildcardChild: Node | null;
|
|
9
|
+
optionalWildcardName: string | null;
|
|
10
|
+
routes: Route.Route.Route[];
|
|
11
|
+
}
|
|
12
|
+
export interface RouteTrie {
|
|
13
|
+
readonly methods: Record<string, Node>;
|
|
14
|
+
}
|
|
15
|
+
export interface LookupResult {
|
|
16
|
+
route: Route.Route.Route;
|
|
17
|
+
params: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export declare function make(set: Route.RouteSet.Any): RouteTrie;
|
|
20
|
+
export declare function lookup(trie: RouteTrie, method: string, path: string): LookupResult[];
|