lakebed 0.0.18 → 0.0.20
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/README.md +46 -30
- package/package.json +5 -5
- package/src/anonymous-server.js +393 -8
- package/src/anonymous.js +241 -0
- package/src/cli.js +401 -79
- package/src/client.d.ts +38 -0
- package/src/client.js +285 -3
- package/src/server.d.ts +49 -0
- package/src/server.js +53 -0
- package/src/source-runtime-worker.js +114 -1
- package/src/source-runtime.js +27 -0
- package/src/version.js +1 -1
package/src/client.d.ts
CHANGED
|
@@ -53,11 +53,49 @@ export type SignInWithGoogleProps = Omit<JSX.ButtonHTMLAttributes<HTMLButtonElem
|
|
|
53
53
|
children?: ComponentChildren;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
export type LakebedLocation = {
|
|
57
|
+
pathname: string;
|
|
58
|
+
search: string;
|
|
59
|
+
hash: string;
|
|
60
|
+
href: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type NavigateOptions = {
|
|
64
|
+
replace?: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type RouterProps = {
|
|
68
|
+
children?: ComponentChildren;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type RoutesProps = {
|
|
72
|
+
children?: ComponentChildren;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type RouteProps = {
|
|
76
|
+
path: string;
|
|
77
|
+
element: ComponentChildren;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type LinkProps = Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
|
|
81
|
+
to: string;
|
|
82
|
+
replace?: boolean;
|
|
83
|
+
children?: ComponentChildren;
|
|
84
|
+
};
|
|
85
|
+
|
|
56
86
|
export function useAuth(): Auth;
|
|
57
87
|
export function signInWithGoogle(options?: SignInWithGoogleOptions): Promise<SignInWithGoogleResult>;
|
|
58
88
|
export function signOut(): void;
|
|
59
89
|
export function getIdentity(): Identity;
|
|
60
90
|
export function decodeIdentityClaims(idToken?: string): IdentityClaims | null;
|
|
61
91
|
export function SignInWithGoogle(props?: SignInWithGoogleProps): JSX.Element;
|
|
92
|
+
export function Router(props?: RouterProps): JSX.Element;
|
|
93
|
+
export function Routes(props?: RoutesProps): JSX.Element | null;
|
|
94
|
+
export function Route(props: RouteProps): JSX.Element | null;
|
|
95
|
+
export function Link(props: LinkProps): JSX.Element;
|
|
96
|
+
export function navigate(to: string, options?: NavigateOptions): void;
|
|
97
|
+
export function useLocation(): LakebedLocation;
|
|
98
|
+
export function useNavigate(): (to: string, options?: NavigateOptions) => void;
|
|
99
|
+
export function useParams<TParams = Record<string, string | undefined>>(): TParams;
|
|
62
100
|
export function useQuery<T>(name: string): T;
|
|
63
101
|
export function useMutation<TArgs extends unknown[], TResult>(name: string): (...args: TArgs) => Promise<TResult>;
|
package/src/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { h } from "preact";
|
|
2
|
-
import { useEffect, useState } from "preact/hooks";
|
|
1
|
+
import { createContext, h, toChildArray } from "preact";
|
|
2
|
+
import { useContext, useEffect, useState } from "preact/hooks";
|
|
3
3
|
|
|
4
4
|
const DEFAULT_SHOO_BASE_URL = "https://shoo.dev";
|
|
5
5
|
const AUTH_STORAGE_KEY = "lakebed_identity";
|
|
@@ -23,6 +23,14 @@ let authInitialized = false;
|
|
|
23
23
|
let authResumeStarted = false;
|
|
24
24
|
let refreshRequested = false;
|
|
25
25
|
|
|
26
|
+
const RouterContext = createContext(null);
|
|
27
|
+
const RouteContext = createContext({ params: {} });
|
|
28
|
+
|
|
29
|
+
function normalizeBasePathValue(value) {
|
|
30
|
+
const clean = String(value ?? "").replace(/\/+$/g, "");
|
|
31
|
+
return clean === "/" ? "" : clean;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function toGuestName(name) {
|
|
27
35
|
return (
|
|
28
36
|
String(name ?? "local")
|
|
@@ -144,7 +152,7 @@ function send(message) {
|
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
function basePath() {
|
|
147
|
-
return window.__LAKEBED_BASE_PATH__ ?? "";
|
|
155
|
+
return normalizeBasePathValue(window.__LAKEBED_BASE_PATH__ ?? "");
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
function authConfig() {
|
|
@@ -159,6 +167,214 @@ function currentRoute() {
|
|
|
159
167
|
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
function appPathnameFromBrowserPathname(pathname) {
|
|
171
|
+
const base = basePath();
|
|
172
|
+
if (!base) {
|
|
173
|
+
return pathname || "/";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (pathname === base) {
|
|
177
|
+
return "/";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (pathname.startsWith(`${base}/`)) {
|
|
181
|
+
return pathname.slice(base.length) || "/";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return pathname || "/";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function currentAppLocation() {
|
|
188
|
+
if (typeof window === "undefined") {
|
|
189
|
+
return { hash: "", href: "/", pathname: "/", search: "" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const pathname = appPathnameFromBrowserPathname(window.location.pathname);
|
|
193
|
+
const search = window.location.search;
|
|
194
|
+
const hash = window.location.hash;
|
|
195
|
+
return {
|
|
196
|
+
hash,
|
|
197
|
+
href: `${pathname}${search}${hash}`,
|
|
198
|
+
pathname,
|
|
199
|
+
search
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isExternalHref(value) {
|
|
204
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value) || value.startsWith("//");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function browserHrefForAppHref(appHref) {
|
|
208
|
+
const url = new URL(appHref, "http://lakebed.local/");
|
|
209
|
+
const base = basePath();
|
|
210
|
+
const pathname = base ? `${base}${url.pathname === "/" ? "/" : url.pathname}` : url.pathname;
|
|
211
|
+
return `${pathname}${url.search}${url.hash}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function hrefForRoute(to) {
|
|
215
|
+
const value = String(to ?? "");
|
|
216
|
+
if (!value) {
|
|
217
|
+
return browserHrefForAppHref(currentAppLocation().href);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isExternalHref(value)) {
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const current = currentAppLocation();
|
|
225
|
+
const resolved = new URL(value, `http://lakebed.local${current.href}`);
|
|
226
|
+
return browserHrefForAppHref(`${resolved.pathname}${resolved.search}${resolved.hash}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function emitLocationChange() {
|
|
230
|
+
window.dispatchEvent(new Event("lakebed:locationchange"));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function navigate(to, options = {}) {
|
|
234
|
+
const href = hrefForRoute(to);
|
|
235
|
+
const parsed = new URL(href, window.location.href);
|
|
236
|
+
|
|
237
|
+
if (parsed.origin !== window.location.origin) {
|
|
238
|
+
window.location.assign(parsed.toString());
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const next = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
243
|
+
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
244
|
+
if (next === current) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (options.replace) {
|
|
249
|
+
window.history.replaceState({}, "", next);
|
|
250
|
+
} else {
|
|
251
|
+
window.history.pushState({}, "", next);
|
|
252
|
+
}
|
|
253
|
+
emitLocationChange();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function useBrowserLocation() {
|
|
257
|
+
const [location, setLocation] = useState(currentAppLocation);
|
|
258
|
+
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
function updateLocation() {
|
|
261
|
+
setLocation(currentAppLocation());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
window.addEventListener("popstate", updateLocation);
|
|
265
|
+
window.addEventListener("lakebed:locationchange", updateLocation);
|
|
266
|
+
return () => {
|
|
267
|
+
window.removeEventListener("popstate", updateLocation);
|
|
268
|
+
window.removeEventListener("lakebed:locationchange", updateLocation);
|
|
269
|
+
};
|
|
270
|
+
}, []);
|
|
271
|
+
|
|
272
|
+
return location;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function normalizeMatchPath(path) {
|
|
276
|
+
const value = String(path ?? "/").trim();
|
|
277
|
+
if (value === "*" || value === "/*") {
|
|
278
|
+
return "*";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const withSlash = value.startsWith("/") ? value : `/${value}`;
|
|
282
|
+
return withSlash.length > 1 ? withSlash.replace(/\/+$/g, "") : "/";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function pathSegments(path) {
|
|
286
|
+
const normalized = normalizeMatchPath(path);
|
|
287
|
+
if (normalized === "*" || normalized === "/") {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return normalized.replace(/^\/+|\/+$/g, "").split("/");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function decodeRouteSegment(value) {
|
|
295
|
+
try {
|
|
296
|
+
return decodeURIComponent(value);
|
|
297
|
+
} catch {
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function matchRoutePath(pattern, pathname) {
|
|
303
|
+
const normalizedPattern = normalizeMatchPath(pattern);
|
|
304
|
+
if (normalizedPattern === "*") {
|
|
305
|
+
return { params: {} };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const patternSegments = pathSegments(normalizedPattern);
|
|
309
|
+
const pathnameSegments = pathSegments(pathname);
|
|
310
|
+
const params = {};
|
|
311
|
+
|
|
312
|
+
for (let index = 0; index < patternSegments.length; index += 1) {
|
|
313
|
+
const patternSegment = patternSegments[index];
|
|
314
|
+
const pathnameSegment = pathnameSegments[index];
|
|
315
|
+
|
|
316
|
+
if (patternSegment === "*") {
|
|
317
|
+
params["*"] = pathnameSegments.slice(index).map(decodeRouteSegment).join("/");
|
|
318
|
+
return { params };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (pathnameSegment === undefined) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (patternSegment.startsWith(":")) {
|
|
326
|
+
const name = patternSegment.slice(1);
|
|
327
|
+
if (!name) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
params[name] = decodeRouteSegment(pathnameSegment);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (patternSegment !== pathnameSegment) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (patternSegments.length !== pathnameSegments.length) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { params };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function routeChildren(children) {
|
|
347
|
+
const routes = [];
|
|
348
|
+
for (const child of toChildArray(children)) {
|
|
349
|
+
if (!child || typeof child !== "object") {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (child.props?.path !== undefined) {
|
|
354
|
+
routes.push(child);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (child.props?.children !== undefined) {
|
|
359
|
+
routes.push(...routeChildren(child.props.children));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return routes;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function shouldHandleLinkClick(event, target) {
|
|
366
|
+
return (
|
|
367
|
+
!event.defaultPrevented &&
|
|
368
|
+
event.button === 0 &&
|
|
369
|
+
!event.altKey &&
|
|
370
|
+
!event.ctrlKey &&
|
|
371
|
+
!event.metaKey &&
|
|
372
|
+
!event.shiftKey &&
|
|
373
|
+
(!target || target === "_self") &&
|
|
374
|
+
!event.currentTarget?.hasAttribute("download")
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
162
378
|
function normalizeReturnTo(value) {
|
|
163
379
|
if (!value) {
|
|
164
380
|
return null;
|
|
@@ -746,6 +962,72 @@ export function SignInWithGoogle({
|
|
|
746
962
|
);
|
|
747
963
|
}
|
|
748
964
|
|
|
965
|
+
export function Router({ children } = {}) {
|
|
966
|
+
const location = useBrowserLocation();
|
|
967
|
+
return h(RouterContext.Provider, { value: { location, navigate } }, children);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function Routes({ children } = {}) {
|
|
971
|
+
const location = useLocation();
|
|
972
|
+
const routes = routeChildren(children);
|
|
973
|
+
for (const route of routes) {
|
|
974
|
+
const match = matchRoutePath(route.props.path, location.pathname);
|
|
975
|
+
if (!match) {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return h(RouteContext.Provider, { value: match }, route.props.element ?? null);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export function Route() {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export function Link({ children, onClick, replace = false, target, to, ...props } = {}) {
|
|
990
|
+
const href = hrefForRoute(to);
|
|
991
|
+
return h(
|
|
992
|
+
"a",
|
|
993
|
+
{
|
|
994
|
+
...props,
|
|
995
|
+
href,
|
|
996
|
+
onClick: (event) => {
|
|
997
|
+
onClick?.(event);
|
|
998
|
+
if (!shouldHandleLinkClick(event, target)) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const parsed = new URL(href, window.location.href);
|
|
1003
|
+
if (parsed.origin !== window.location.origin) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
event.preventDefault();
|
|
1008
|
+
navigate(to, { replace });
|
|
1009
|
+
},
|
|
1010
|
+
target
|
|
1011
|
+
},
|
|
1012
|
+
children
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function useLocation() {
|
|
1017
|
+
const context = useContext(RouterContext);
|
|
1018
|
+
const fallback = useBrowserLocation();
|
|
1019
|
+
return context?.location ?? fallback;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export function useNavigate() {
|
|
1023
|
+
const context = useContext(RouterContext);
|
|
1024
|
+
return context?.navigate ?? navigate;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export function useParams() {
|
|
1028
|
+
return useContext(RouteContext).params;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
749
1031
|
export function useQuery(name) {
|
|
750
1032
|
const [value, setValue] = useState(queryValues.get(name) ?? []);
|
|
751
1033
|
|
package/src/server.d.ts
CHANGED
|
@@ -49,11 +49,60 @@ export type ServerContext = {
|
|
|
49
49
|
log: LogContext;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
export type EndpointRoute = {
|
|
53
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD" | (string & {});
|
|
54
|
+
path: `/${string}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type EndpointHeaders = {
|
|
58
|
+
get(name: string): string | null;
|
|
59
|
+
has(name: string): boolean;
|
|
60
|
+
entries(): IterableIterator<[string, string]>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type EndpointRequest = {
|
|
64
|
+
method: string;
|
|
65
|
+
path: string;
|
|
66
|
+
url: string;
|
|
67
|
+
headers: EndpointHeaders;
|
|
68
|
+
query: URLSearchParams;
|
|
69
|
+
text(): Promise<string>;
|
|
70
|
+
json<T = unknown>(): Promise<T>;
|
|
71
|
+
bytes(): Promise<Uint8Array>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type EndpointResponse = {
|
|
75
|
+
kind: "response";
|
|
76
|
+
status: number;
|
|
77
|
+
headers?: Record<string, string>;
|
|
78
|
+
body?: string | Uint8Array | ArrayBuffer;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type EndpointResponseOptions = {
|
|
82
|
+
status?: number;
|
|
83
|
+
headers?: Record<string, string>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type EndpointDefinition<TResult = EndpointResponse | string | unknown | void> = {
|
|
87
|
+
kind: "endpoint";
|
|
88
|
+
method: string;
|
|
89
|
+
path: string;
|
|
90
|
+
handler: (ctx: ServerContext, req: EndpointRequest) => TResult | Promise<TResult>;
|
|
91
|
+
};
|
|
92
|
+
|
|
52
93
|
export function capsule<T>(definition: T): T;
|
|
53
94
|
export function query<T>(handler: (ctx: ServerContext) => T): (ctx: ServerContext) => T;
|
|
54
95
|
export function mutation<TArgs extends unknown[], TResult>(
|
|
55
96
|
handler: (ctx: ServerContext, ...args: TArgs) => TResult
|
|
56
97
|
): (ctx: ServerContext, ...args: TArgs) => TResult;
|
|
98
|
+
export function endpoint<TResult>(
|
|
99
|
+
route: EndpointRoute,
|
|
100
|
+
handler: (ctx: ServerContext, req: EndpointRequest) => TResult | Promise<TResult>
|
|
101
|
+
): EndpointDefinition<TResult>;
|
|
102
|
+
export function json(value: unknown, options?: EndpointResponseOptions): EndpointResponse;
|
|
103
|
+
export function text(value: unknown, options?: EndpointResponseOptions): EndpointResponse;
|
|
104
|
+
export function empty(options?: EndpointResponseOptions): EndpointResponse;
|
|
105
|
+
export function redirect(url: string, options?: EndpointResponseOptions): EndpointResponse;
|
|
57
106
|
export function table(fields: Record<string, Field<unknown>>): TableDefinition;
|
|
58
107
|
export function string(): Field<string>;
|
|
59
108
|
export function boolean(): Field<boolean>;
|
package/src/server.js
CHANGED
|
@@ -10,6 +10,59 @@ export function mutation(handler) {
|
|
|
10
10
|
return handler;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function endpoint(route, handler) {
|
|
14
|
+
return {
|
|
15
|
+
handler,
|
|
16
|
+
kind: "endpoint",
|
|
17
|
+
method: String(route?.method ?? "").toUpperCase(),
|
|
18
|
+
path: String(route?.path ?? "")
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function response(body, { headers = {}, status = 200 } = {}) {
|
|
23
|
+
return {
|
|
24
|
+
body,
|
|
25
|
+
headers,
|
|
26
|
+
kind: "response",
|
|
27
|
+
status
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function json(value, options = {}) {
|
|
32
|
+
return response(JSON.stringify(value ?? null), {
|
|
33
|
+
...options,
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
36
|
+
...(options.headers ?? {})
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function text(value, options = {}) {
|
|
42
|
+
return response(String(value ?? ""), {
|
|
43
|
+
...options,
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
46
|
+
...(options.headers ?? {})
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function empty(options = {}) {
|
|
52
|
+
return response("", { status: 204, ...options });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function redirect(url, options = {}) {
|
|
56
|
+
return response("", {
|
|
57
|
+
status: 302,
|
|
58
|
+
...options,
|
|
59
|
+
headers: {
|
|
60
|
+
Location: String(url),
|
|
61
|
+
...(options.headers ?? {})
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
13
66
|
function field(kind) {
|
|
14
67
|
return {
|
|
15
68
|
kind,
|
|
@@ -312,6 +312,103 @@ function createBrokeredResponse(response) {
|
|
|
312
312
|
};
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
function endpointBodyToBase64(body) {
|
|
316
|
+
if (body === undefined || body === null) {
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
if (typeof body === "string") {
|
|
320
|
+
return nodeBuffer.from(body, "utf8").toString("base64");
|
|
321
|
+
}
|
|
322
|
+
if (body instanceof ArrayBuffer) {
|
|
323
|
+
return nodeBuffer.from(body).toString("base64");
|
|
324
|
+
}
|
|
325
|
+
if (ArrayBuffer.isView(body)) {
|
|
326
|
+
return nodeBuffer.from(body.buffer, body.byteOffset, body.byteLength).toString("base64");
|
|
327
|
+
}
|
|
328
|
+
return nodeBuffer.from(JSON.stringify(body ?? null), "utf8").toString("base64");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function endpointStatus(status, fallback = 200) {
|
|
332
|
+
const parsed = Number(status);
|
|
333
|
+
return Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : fallback;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function normalizeEndpointResponse(result) {
|
|
337
|
+
if (result === undefined || result === null) {
|
|
338
|
+
return { bodyBase64: "", headers: {}, status: 204 };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
result &&
|
|
343
|
+
typeof result === "object" &&
|
|
344
|
+
result.kind !== "response" &&
|
|
345
|
+
typeof result.arrayBuffer === "function" &&
|
|
346
|
+
typeof result.status === "number"
|
|
347
|
+
) {
|
|
348
|
+
const body = nodeBuffer.from(await result.arrayBuffer());
|
|
349
|
+
return {
|
|
350
|
+
bodyBase64: body.toString("base64"),
|
|
351
|
+
headers: headersToObject(result.headers),
|
|
352
|
+
status: endpointStatus(result.status)
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (isPlainObject(result) && result.kind === "response") {
|
|
357
|
+
return {
|
|
358
|
+
bodyBase64: endpointBodyToBase64(result.body),
|
|
359
|
+
headers: headersToObject(result.headers),
|
|
360
|
+
status: endpointStatus(result.status)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (typeof result === "string") {
|
|
365
|
+
return {
|
|
366
|
+
bodyBase64: endpointBodyToBase64(result),
|
|
367
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
368
|
+
status: 200
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
bodyBase64: endpointBodyToBase64(result),
|
|
374
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
375
|
+
status: 200
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function createEndpointRequest(request) {
|
|
380
|
+
const url = new URL(request.url ?? request.path ?? "/", "http://lakebed.local");
|
|
381
|
+
const body = nodeBuffer.from(request.bodyBase64 ?? "", "base64");
|
|
382
|
+
const headers = new Map(Object.entries(headersToObject(request.headers)).map(([key, value]) => [key.toLowerCase(), String(value)]));
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
headers: {
|
|
386
|
+
entries() {
|
|
387
|
+
return headers.entries();
|
|
388
|
+
},
|
|
389
|
+
get(name) {
|
|
390
|
+
return headers.get(String(name).toLowerCase()) ?? null;
|
|
391
|
+
},
|
|
392
|
+
has(name) {
|
|
393
|
+
return headers.has(String(name).toLowerCase());
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
method: String(request.method ?? "GET").toUpperCase(),
|
|
397
|
+
path: request.path ?? url.pathname,
|
|
398
|
+
query: new URLSearchParams(url.searchParams),
|
|
399
|
+
url: url.href,
|
|
400
|
+
async bytes() {
|
|
401
|
+
return new Uint8Array(body);
|
|
402
|
+
},
|
|
403
|
+
async json() {
|
|
404
|
+
return JSON.parse(body.toString("utf8"));
|
|
405
|
+
},
|
|
406
|
+
async text() {
|
|
407
|
+
return body.toString("utf8");
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
315
412
|
function brokeredFetch(input, init = {}) {
|
|
316
413
|
if (!allowBrokeredFetch) {
|
|
317
414
|
return Promise.reject(new Error("Outbound fetch is disabled for anonymous deploys."));
|
|
@@ -389,7 +486,8 @@ function sendError(error) {
|
|
|
389
486
|
sendToParent?.({
|
|
390
487
|
error: {
|
|
391
488
|
message: error instanceof Error ? error.message : String(error),
|
|
392
|
-
name: error instanceof Error ? error.name : "Error"
|
|
489
|
+
name: error instanceof Error ? error.name : "Error",
|
|
490
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
393
491
|
},
|
|
394
492
|
ok: false,
|
|
395
493
|
type: "source-runtime.result"
|
|
@@ -429,6 +527,21 @@ async function runSource(request) {
|
|
|
429
527
|
return;
|
|
430
528
|
}
|
|
431
529
|
|
|
530
|
+
if (request.op === "endpoint") {
|
|
531
|
+
const definition = app.endpoints?.[request.name];
|
|
532
|
+
const handler = definition?.handler ?? definition;
|
|
533
|
+
if (typeof handler !== "function") {
|
|
534
|
+
throw new Error(`Unknown endpoint: ${request.name}`);
|
|
535
|
+
}
|
|
536
|
+
const result = await handler(source.ctx, createEndpointRequest(request.endpointRequest ?? {}));
|
|
537
|
+
const response = await normalizeEndpointResponse(result);
|
|
538
|
+
sendResult({
|
|
539
|
+
operations: jsonSafe(source.operations),
|
|
540
|
+
response: jsonSafe(response)
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
432
545
|
throw new Error(`Unsupported source operation: ${request.op}`);
|
|
433
546
|
}
|
|
434
547
|
|
package/src/source-runtime.js
CHANGED
|
@@ -493,6 +493,9 @@ async function brokeredFetch(request, options) {
|
|
|
493
493
|
function workerError(payload) {
|
|
494
494
|
const error = new Error(payload?.message ?? "Source runtime failed.");
|
|
495
495
|
error.name = payload?.name ?? "SourceRuntimeError";
|
|
496
|
+
if (payload?.stack) {
|
|
497
|
+
error.stack = payload.stack;
|
|
498
|
+
}
|
|
496
499
|
return error;
|
|
497
500
|
}
|
|
498
501
|
|
|
@@ -557,6 +560,29 @@ export class ChildProcessSourceRuntime {
|
|
|
557
560
|
}, mutationTransactionOptions(limits));
|
|
558
561
|
}
|
|
559
562
|
|
|
563
|
+
async executeEndpoint({ artifact, auth, deployId, limits = DEFAULT_ANONYMOUS_LIMITS, name, request, state }) {
|
|
564
|
+
return state.transaction(deployId, async (tx) => {
|
|
565
|
+
const snapshot = await snapshotSourceState({ artifact, deployId, state: tx });
|
|
566
|
+
const response = await this.runWorker({
|
|
567
|
+
allowFetch: artifact.deployTarget === "claimed-source",
|
|
568
|
+
artifact,
|
|
569
|
+
auth,
|
|
570
|
+
endpointRequest: request,
|
|
571
|
+
env: snapshot.env,
|
|
572
|
+
name,
|
|
573
|
+
op: "endpoint",
|
|
574
|
+
rows: snapshot.rows
|
|
575
|
+
});
|
|
576
|
+
await applySourceOperations({
|
|
577
|
+
artifact,
|
|
578
|
+
deployId,
|
|
579
|
+
operations: response.operations ?? [],
|
|
580
|
+
tx
|
|
581
|
+
});
|
|
582
|
+
return response.response ?? { bodyBase64: "", headers: {}, status: 204 };
|
|
583
|
+
}, mutationTransactionOptions(limits));
|
|
584
|
+
}
|
|
585
|
+
|
|
560
586
|
runWorker(request) {
|
|
561
587
|
return new Promise((resolveRun, rejectRun) => {
|
|
562
588
|
const child = fork(this.workerPath, [], {
|
|
@@ -603,6 +629,7 @@ export class ChildProcessSourceRuntime {
|
|
|
603
629
|
if (message.ok) {
|
|
604
630
|
finish(resolveRun, {
|
|
605
631
|
operations: message.operations,
|
|
632
|
+
response: message.response,
|
|
606
633
|
result: message.result
|
|
607
634
|
});
|
|
608
635
|
} else {
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LAKEBED_VERSION = "0.0.
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.20";
|