@voyant-travel/runtime 0.3.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 (59) hide show
  1. package/README.md +54 -0
  2. package/dist/api-dispatch.d.ts +44 -0
  3. package/dist/api-dispatch.d.ts.map +1 -0
  4. package/dist/api-dispatch.js +64 -0
  5. package/dist/env.d.ts +33 -0
  6. package/dist/env.d.ts.map +1 -0
  7. package/dist/env.js +30 -0
  8. package/dist/env.test.d.ts +2 -0
  9. package/dist/env.test.d.ts.map +1 -0
  10. package/dist/env.test.js +21 -0
  11. package/dist/index.d.ts +21 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +10 -0
  14. package/dist/memory-kv.d.ts +49 -0
  15. package/dist/memory-kv.d.ts.map +1 -0
  16. package/dist/memory-kv.js +69 -0
  17. package/dist/memory-kv.test.d.ts +2 -0
  18. package/dist/memory-kv.test.d.ts.map +1 -0
  19. package/dist/memory-kv.test.js +44 -0
  20. package/dist/memory-r2.d.ts +4 -0
  21. package/dist/memory-r2.d.ts.map +1 -0
  22. package/dist/memory-r2.js +76 -0
  23. package/dist/memory-r2.test.d.ts +2 -0
  24. package/dist/memory-r2.test.d.ts.map +1 -0
  25. package/dist/memory-r2.test.js +45 -0
  26. package/dist/node-server.d.ts +56 -0
  27. package/dist/node-server.d.ts.map +1 -0
  28. package/dist/node-server.js +74 -0
  29. package/dist/node-server.test.d.ts +2 -0
  30. package/dist/node-server.test.d.ts.map +1 -0
  31. package/dist/node-server.test.js +86 -0
  32. package/dist/r2.d.ts +41 -0
  33. package/dist/r2.d.ts.map +1 -0
  34. package/dist/r2.js +147 -0
  35. package/dist/r2.test.d.ts +2 -0
  36. package/dist/r2.test.d.ts.map +1 -0
  37. package/dist/r2.test.js +81 -0
  38. package/dist/ssr-manifest.d.ts +51 -0
  39. package/dist/ssr-manifest.d.ts.map +1 -0
  40. package/dist/ssr-manifest.js +49 -0
  41. package/dist/trust.d.ts +31 -0
  42. package/dist/trust.d.ts.map +1 -0
  43. package/dist/trust.js +49 -0
  44. package/dist/trust.test.d.ts +2 -0
  45. package/dist/trust.test.d.ts.map +1 -0
  46. package/dist/trust.test.js +42 -0
  47. package/dist/types.d.ts +54 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +1 -0
  50. package/dist/wait-until.d.ts +25 -0
  51. package/dist/wait-until.d.ts.map +1 -0
  52. package/dist/wait-until.js +50 -0
  53. package/dist/wait-until.test.d.ts +2 -0
  54. package/dist/wait-until.test.d.ts.map +1 -0
  55. package/dist/wait-until.test.js +40 -0
  56. package/dist/worker-fetch.d.ts +31 -0
  57. package/dist/worker-fetch.d.ts.map +1 -0
  58. package/dist/worker-fetch.js +37 -0
  59. package/package.json +114 -0
package/dist/trust.js ADDED
@@ -0,0 +1,49 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ /**
3
+ * Pinned cross-repo constant: the header the platform dispatcher stamps on
4
+ * every request it forwards to a dedicated (Cloud Run) app. The value is a
5
+ * per-app shared secret. Do NOT rename — the platform side depends on it.
6
+ */
7
+ export const ORIGIN_TRUST_HEADER = "x-voyant-origin-trust";
8
+ /**
9
+ * Constant-time string comparison. Returns `false` for length mismatches
10
+ * without leaking the comparison via early return timing (the length check is
11
+ * unavoidable and is itself not secret-length-dependent beyond equality).
12
+ */
13
+ export function constantTimeEqual(a, b) {
14
+ const aBytes = new TextEncoder().encode(a);
15
+ const bBytes = new TextEncoder().encode(b);
16
+ if (aBytes.length !== bBytes.length) {
17
+ // Still burn a comparison against a same-length buffer so a size probe
18
+ // does not short-circuit measurably faster than a value mismatch.
19
+ timingSafeEqual(aBytes, aBytes);
20
+ return false;
21
+ }
22
+ return timingSafeEqual(aBytes, bBytes);
23
+ }
24
+ /**
25
+ * Verify that a request carries a valid origin-trust header for `secret`.
26
+ */
27
+ export function verifyOriginTrust(request, secret) {
28
+ const provided = request.headers.get(ORIGIN_TRUST_HEADER);
29
+ if (provided === null)
30
+ return false;
31
+ return constantTimeEqual(provided, secret);
32
+ }
33
+ /**
34
+ * Build a low-level trust gate. Returns a function that, given a request,
35
+ * yields a `403` {@link Response} when the request must be rejected, or
36
+ * `undefined` when it may proceed. Composable — used by {@link createNodeServer}
37
+ * but exported so callers can wire it into their own server loop.
38
+ */
39
+ export function originTrustMiddleware(secret, options = {}) {
40
+ const exempt = new Set(options.exemptPaths ?? ["/healthz"]);
41
+ return (request) => {
42
+ const { pathname } = new URL(request.url);
43
+ if (exempt.has(pathname))
44
+ return undefined;
45
+ if (verifyOriginTrust(request, secret))
46
+ return undefined;
47
+ return new Response("Forbidden: invalid origin trust", { status: 403 });
48
+ };
49
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=trust.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust.test.d.ts","sourceRoot":"","sources":["../src/trust.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { constantTimeEqual, ORIGIN_TRUST_HEADER, originTrustMiddleware, verifyOriginTrust, } from "./trust.js";
3
+ function req(headers = {}, path = "/v1/admin/x") {
4
+ return new Request(`https://app.example.com${path}`, { headers });
5
+ }
6
+ describe("constantTimeEqual", () => {
7
+ it("returns true for equal strings", () => {
8
+ expect(constantTimeEqual("abc", "abc")).toBe(true);
9
+ });
10
+ it("returns false for different values and lengths", () => {
11
+ expect(constantTimeEqual("abc", "abd")).toBe(false);
12
+ expect(constantTimeEqual("abc", "abcd")).toBe(false);
13
+ expect(constantTimeEqual("", "x")).toBe(false);
14
+ });
15
+ });
16
+ describe("verifyOriginTrust", () => {
17
+ it("passes when the header matches the secret", () => {
18
+ expect(verifyOriginTrust(req({ [ORIGIN_TRUST_HEADER]: "s3cr3t" }), "s3cr3t")).toBe(true);
19
+ });
20
+ it("fails when header is missing or wrong", () => {
21
+ expect(verifyOriginTrust(req(), "s3cr3t")).toBe(false);
22
+ expect(verifyOriginTrust(req({ [ORIGIN_TRUST_HEADER]: "nope" }), "s3cr3t")).toBe(false);
23
+ });
24
+ });
25
+ describe("originTrustMiddleware", () => {
26
+ it("rejects unauthenticated requests with 403", () => {
27
+ const gate = originTrustMiddleware("s3cr3t");
28
+ const rejection = gate(req());
29
+ expect(rejection?.status).toBe(403);
30
+ });
31
+ it("allows authenticated requests through (undefined)", () => {
32
+ const gate = originTrustMiddleware("s3cr3t");
33
+ expect(gate(req({ [ORIGIN_TRUST_HEADER]: "s3cr3t" }))).toBeUndefined();
34
+ });
35
+ it("exempts configured paths", () => {
36
+ const gate = originTrustMiddleware("s3cr3t", { exemptPaths: ["/healthz"] });
37
+ expect(gate(req({}, "/healthz"))).toBeUndefined();
38
+ });
39
+ it("uses the pinned header name", () => {
40
+ expect(ORIGIN_TRUST_HEADER).toBe("x-voyant-origin-trust");
41
+ });
42
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Minimal structural slice of the Cloudflare `ExecutionContext`. Only
3
+ * `waitUntil` is required by this package; the full context object is passed
4
+ * through to apps untouched, so any richer context type remains compatible.
5
+ */
6
+ export interface WaitUntilContext {
7
+ waitUntil(promise: Promise<unknown>): void;
8
+ }
9
+ /**
10
+ * Anything with a Worker-style `fetch` — a Hono app, a Better Auth handler,
11
+ * or a plain `{ fetch }` module default export.
12
+ */
13
+ export interface FetchApp<Env = unknown, Ctx extends WaitUntilContext = WaitUntilContext> {
14
+ fetch(request: Request, env?: Env, ctx?: Ctx): Response | Promise<Response>;
15
+ }
16
+ /**
17
+ * Lazily resolves a {@link FetchApp}. Loaders own the dynamic `import()` of
18
+ * app modules so the Worker entry stays out of the API graph until a request
19
+ * actually needs it.
20
+ */
21
+ export type AppLoader<Env = unknown, Ctx extends WaitUntilContext = WaitUntilContext> = () => Promise<FetchApp<Env, Ctx>>;
22
+ /**
23
+ * Structural slice of the Cloudflare Workers `ExecutionContext`. On a resident
24
+ * Node process we can ship a *real* {@link ExecutionContextLike.waitUntil} that
25
+ * tracks background work for graceful shutdown, instead of running subscriber
26
+ * work inline (which adds request latency) or dropping it on exit.
27
+ */
28
+ export interface ExecutionContextLike extends WaitUntilContext {
29
+ /**
30
+ * No-op on Node — kept for source-compatibility with app code that calls
31
+ * `ctx.passThroughOnException()` on Workers.
32
+ */
33
+ passThroughOnException?(): void;
34
+ }
35
+ /**
36
+ * Worker-style `fetch(request, env, ctx)` entrypoint. This is the exact shape
37
+ * an operator app exports; the whole point of this package is that the same
38
+ * handler runs unchanged on Node via {@link createNodeServer}.
39
+ */
40
+ export type FetchHandler<Env> = (request: Request, env: Env, ctx: ExecutionContextLike) => Response | Promise<Response>;
41
+ /**
42
+ * The event object passed to a Worker `scheduled()` handler. On Cloud Run the
43
+ * cron triggers arrive over HTTP (Cloud Scheduler), so `scheduledTime` is the
44
+ * dispatch time and `cron` is echoed from the request.
45
+ */
46
+ export interface ScheduledEventLike {
47
+ cron: string;
48
+ scheduledTime: number;
49
+ }
50
+ /**
51
+ * Worker-style `scheduled(event, env, ctx)` handler.
52
+ */
53
+ export type ScheduledHandler<Env> = (event: ScheduledEventLike, env: Env, ctx: ExecutionContextLike) => void | Promise<void>;
54
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ,CAAC,GAAG,GAAG,OAAO,EAAE,GAAG,SAAS,gBAAgB,GAAG,gBAAgB;IACtF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC5E;AAED;;;;GAIG;AACH,MAAM,MAAM,SAAS,CACnB,GAAG,GAAG,OAAO,EACb,GAAG,SAAS,gBAAgB,GAAG,gBAAgB,IAC7C,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;AAErC;;;;;GAKG;AACH,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC5D;;;OAGG;IACH,sBAAsB,CAAC,IAAI,IAAI,CAAA;CAChC;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,IAAI,CAC9B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,oBAAoB,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEjC;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAAC,GAAG,IAAI,CAClC,KAAK,EAAE,kBAAkB,EACzB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,oBAAoB,KACtB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { ExecutionContextLike } from "./types.js";
2
+ export interface WaitUntilRegistry {
3
+ /**
4
+ * Produce a fresh {@link ExecutionContextLike} for a single request. Every
5
+ * promise passed to its `waitUntil` is tracked centrally so the process can
6
+ * drain in-flight background work on shutdown.
7
+ */
8
+ context(): ExecutionContextLike;
9
+ /**
10
+ * Wait for all currently-tracked promises to settle, or until `timeoutMs`
11
+ * elapses — whichever comes first. Resolves (never rejects); individual
12
+ * promise rejections are swallowed here since `waitUntil` work is
13
+ * fire-and-forget by the Workers contract.
14
+ */
15
+ drain(timeoutMs?: number): Promise<void>;
16
+ /** Number of promises still in flight. Useful for tests and readiness. */
17
+ pending(): number;
18
+ }
19
+ /**
20
+ * Track every `waitUntil` promise across requests so a resident Node process
21
+ * can drain background work (outbox publishes, cache warms, event-bus fan-out)
22
+ * before it exits — the behavior Cloudflare gives for free but Node does not.
23
+ */
24
+ export declare function createWaitUntilRegistry(): WaitUntilRegistry;
25
+ //# sourceMappingURL=wait-until.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-until.d.ts","sourceRoot":"","sources":["../src/wait-until.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,IAAI,oBAAoB,CAAA;IAC/B;;;;;OAKG;IACH,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,0EAA0E;IAC1E,OAAO,IAAI,MAAM,CAAA;CAClB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,iBAAiB,CA4C3D"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Track every `waitUntil` promise across requests so a resident Node process
3
+ * can drain background work (outbox publishes, cache warms, event-bus fan-out)
4
+ * before it exits — the behavior Cloudflare gives for free but Node does not.
5
+ */
6
+ export function createWaitUntilRegistry() {
7
+ const inflight = new Set();
8
+ function track(promise) {
9
+ const tracked = Promise.resolve(promise).catch(() => {
10
+ // waitUntil work is fire-and-forget; never let a rejection escape.
11
+ });
12
+ inflight.add(tracked);
13
+ void tracked.finally(() => {
14
+ inflight.delete(tracked);
15
+ });
16
+ }
17
+ return {
18
+ context() {
19
+ return {
20
+ waitUntil(promise) {
21
+ track(promise);
22
+ },
23
+ passThroughOnException() {
24
+ // no-op on Node
25
+ },
26
+ };
27
+ },
28
+ async drain(timeoutMs = 10_000) {
29
+ if (inflight.size === 0)
30
+ return;
31
+ const settled = Promise.allSettled([...inflight]).then(() => undefined);
32
+ if (timeoutMs <= 0) {
33
+ await settled;
34
+ return;
35
+ }
36
+ let timer;
37
+ const timeout = new Promise((resolve) => {
38
+ timer = setTimeout(resolve, timeoutMs);
39
+ // Do not keep the event loop alive purely for the drain timeout.
40
+ timer.unref?.();
41
+ });
42
+ await Promise.race([settled, timeout]);
43
+ if (timer)
44
+ clearTimeout(timer);
45
+ },
46
+ pending() {
47
+ return inflight.size;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=wait-until.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-until.test.d.ts","sourceRoot":"","sources":["../src/wait-until.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createWaitUntilRegistry } from "./wait-until.js";
3
+ const tick = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
+ describe("createWaitUntilRegistry", () => {
5
+ it("tracks pending promises and drains them", async () => {
6
+ const registry = createWaitUntilRegistry();
7
+ const ctx = registry.context();
8
+ let done = false;
9
+ ctx.waitUntil(tick(20).then(() => (done = true)));
10
+ expect(registry.pending()).toBe(1);
11
+ await registry.drain(1000);
12
+ expect(done).toBe(true);
13
+ expect(registry.pending()).toBe(0);
14
+ });
15
+ it("resolves immediately when nothing is in flight", async () => {
16
+ const registry = createWaitUntilRegistry();
17
+ await expect(registry.drain(1000)).resolves.toBeUndefined();
18
+ });
19
+ it("stops waiting after the timeout for slow work", async () => {
20
+ const registry = createWaitUntilRegistry();
21
+ registry.context().waitUntil(tick(5000));
22
+ const start = Date.now();
23
+ await registry.drain(30);
24
+ expect(Date.now() - start).toBeLessThan(1000);
25
+ });
26
+ it("swallows rejections so drain never throws", async () => {
27
+ const registry = createWaitUntilRegistry();
28
+ registry.context().waitUntil(Promise.reject(new Error("boom")));
29
+ await expect(registry.drain(1000)).resolves.toBeUndefined();
30
+ expect(registry.pending()).toBe(0);
31
+ });
32
+ it("shares one registry across multiple request contexts", async () => {
33
+ const registry = createWaitUntilRegistry();
34
+ registry.context().waitUntil(tick(10));
35
+ registry.context().waitUntil(tick(10));
36
+ expect(registry.pending()).toBe(2);
37
+ await registry.drain(1000);
38
+ expect(registry.pending()).toBe(0);
39
+ });
40
+ });
@@ -0,0 +1,31 @@
1
+ import { type ApiDispatch, type CreateApiDispatchOptions } from "./api-dispatch.js";
2
+ import type { WaitUntilContext } from "./types.js";
3
+ export type SsrHandler<Env, Ctx extends WaitUntilContext = WaitUntilContext> = (request: Request, env: Env, ctx: Ctx) => Response | Promise<Response>;
4
+ /** Loads the real SSR handler. Wrap with {@link lazySsr}. */
5
+ export type SsrLoader<Env, Ctx extends WaitUntilContext = WaitUntilContext> = () => Promise<SsrHandler<Env, Ctx>>;
6
+ /**
7
+ * Memoize an SSR-handler loader so the dynamic `import()` of the TanStack Start
8
+ * server graph — React + `react-dom/server` — runs once per isolate and stays
9
+ * OUT of the Worker's startup graph. The SSR handler is only invoked on
10
+ * non-API requests, so API-only isolates never load the React SSR graph at all:
11
+ * the same win `lazyApp` gives the Hono API, applied to the SSR half.
12
+ *
13
+ * Without this, `entry.ts` statically importing `@tanstack/react-start/server`
14
+ * pulls ~2.2 MB of React/`react-dom/server` into module global scope, which
15
+ * Cloudflare parses on every cold isolate before it can answer even
16
+ * `/api/health`.
17
+ */
18
+ export declare function lazySsr<Env, Ctx extends WaitUntilContext = WaitUntilContext>(load: SsrLoader<Env, Ctx>): SsrHandler<Env, Ctx>;
19
+ export interface CreateWorkerFetchOptions<Env, Ctx extends WaitUntilContext = WaitUntilContext> {
20
+ /** An {@link ApiDispatch} instance, or options to build one. */
21
+ api: ApiDispatch<Env, Ctx> | CreateApiDispatchOptions<Env, Ctx>;
22
+ /** Handles every non-API request (typically the SSR start handler). */
23
+ ssr: SsrHandler<Env, Ctx>;
24
+ }
25
+ /**
26
+ * The Worker `fetch` entrypoint: API-prefixed requests go through the
27
+ * prefix-stripping dispatch, everything else goes to SSR. The app's
28
+ * `entry.ts` shrinks to bindings plus this factory call.
29
+ */
30
+ export declare function createWorkerFetch<Env, Ctx extends WaitUntilContext = WaitUntilContext>(options: CreateWorkerFetchOptions<Env, Ctx>): (request: Request, env: Env, ctx: Ctx) => Promise<Response>;
31
+ //# sourceMappingURL=worker-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-fetch.d.ts","sourceRoot":"","sources":["../src/worker-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAE9B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD,MAAM,MAAM,UAAU,CAAC,GAAG,EAAE,GAAG,SAAS,gBAAgB,GAAG,gBAAgB,IAAI,CAC7E,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,GAAG,KACL,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEjC,6DAA6D;AAC7D,MAAM,MAAM,SAAS,CAAC,GAAG,EAAE,GAAG,SAAS,gBAAgB,GAAG,gBAAgB,IAAI,MAAM,OAAO,CACzF,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CACrB,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,SAAS,gBAAgB,GAAG,gBAAgB,EAC1E,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,GACxB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAMtB;AAED,MAAM,WAAW,wBAAwB,CAAC,GAAG,EAAE,GAAG,SAAS,gBAAgB,GAAG,gBAAgB;IAC5F,gEAAgE;IAChE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,wBAAwB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAC/D,uEAAuE;IACvE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;CAC1B;AAQD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,SAAS,gBAAgB,GAAG,gBAAgB,EACpF,OAAO,EAAE,wBAAwB,CAAC,GAAG,EAAE,GAAG,CAAC,GAC1C,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAS7D"}
@@ -0,0 +1,37 @@
1
+ import { createApiDispatch, } from "./api-dispatch.js";
2
+ /**
3
+ * Memoize an SSR-handler loader so the dynamic `import()` of the TanStack Start
4
+ * server graph — React + `react-dom/server` — runs once per isolate and stays
5
+ * OUT of the Worker's startup graph. The SSR handler is only invoked on
6
+ * non-API requests, so API-only isolates never load the React SSR graph at all:
7
+ * the same win `lazyApp` gives the Hono API, applied to the SSR half.
8
+ *
9
+ * Without this, `entry.ts` statically importing `@tanstack/react-start/server`
10
+ * pulls ~2.2 MB of React/`react-dom/server` into module global scope, which
11
+ * Cloudflare parses on every cold isolate before it can answer even
12
+ * `/api/health`.
13
+ */
14
+ export function lazySsr(load) {
15
+ let promise;
16
+ return (request, env, ctx) => {
17
+ promise ??= load();
18
+ return promise.then((handler) => handler(request, env, ctx));
19
+ };
20
+ }
21
+ function isApiDispatch(api) {
22
+ return "dispatch" in api;
23
+ }
24
+ /**
25
+ * The Worker `fetch` entrypoint: API-prefixed requests go through the
26
+ * prefix-stripping dispatch, everything else goes to SSR. The app's
27
+ * `entry.ts` shrinks to bindings plus this factory call.
28
+ */
29
+ export function createWorkerFetch(options) {
30
+ const api = isApiDispatch(options.api) ? options.api : createApiDispatch(options.api);
31
+ return async (request, env, ctx) => {
32
+ if (api.isApiRequest(new URL(request.url).pathname)) {
33
+ return api.dispatch(request, env, ctx);
34
+ }
35
+ return options.ssr(request, env, ctx);
36
+ };
37
+ }
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@voyant-travel/runtime",
3
+ "version": "0.3.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./api-dispatch": "./src/api-dispatch.ts",
10
+ "./ssr-manifest": "./src/ssr-manifest.ts",
11
+ "./worker-fetch": "./src/worker-fetch.ts",
12
+ "./node-server": "./src/node-server.ts",
13
+ "./trust": "./src/trust.ts",
14
+ "./wait-until": "./src/wait-until.ts",
15
+ "./env": "./src/env.ts",
16
+ "./memory-kv": "./src/memory-kv.ts",
17
+ "./memory-r2": "./src/memory-r2.ts",
18
+ "./r2": "./src/r2.ts",
19
+ "./types": "./src/types.ts"
20
+ },
21
+ "scripts": {
22
+ "typecheck": "tsc -p tsconfig.typecheck.json",
23
+ "lint": "biome check src/ tests/",
24
+ "test": "vitest run",
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
27
+ "prepack": "pnpm run build"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./api-dispatch": {
41
+ "types": "./dist/api-dispatch.d.ts",
42
+ "import": "./dist/api-dispatch.js",
43
+ "default": "./dist/api-dispatch.js"
44
+ },
45
+ "./ssr-manifest": {
46
+ "types": "./dist/ssr-manifest.d.ts",
47
+ "import": "./dist/ssr-manifest.js",
48
+ "default": "./dist/ssr-manifest.js"
49
+ },
50
+ "./worker-fetch": {
51
+ "types": "./dist/worker-fetch.d.ts",
52
+ "import": "./dist/worker-fetch.js",
53
+ "default": "./dist/worker-fetch.js"
54
+ },
55
+ "./node-server": {
56
+ "types": "./dist/node-server.d.ts",
57
+ "import": "./dist/node-server.js",
58
+ "default": "./dist/node-server.js"
59
+ },
60
+ "./trust": {
61
+ "types": "./dist/trust.d.ts",
62
+ "import": "./dist/trust.js",
63
+ "default": "./dist/trust.js"
64
+ },
65
+ "./wait-until": {
66
+ "types": "./dist/wait-until.d.ts",
67
+ "import": "./dist/wait-until.js",
68
+ "default": "./dist/wait-until.js"
69
+ },
70
+ "./env": {
71
+ "types": "./dist/env.d.ts",
72
+ "import": "./dist/env.js",
73
+ "default": "./dist/env.js"
74
+ },
75
+ "./memory-kv": {
76
+ "types": "./dist/memory-kv.d.ts",
77
+ "import": "./dist/memory-kv.js",
78
+ "default": "./dist/memory-kv.js"
79
+ },
80
+ "./memory-r2": {
81
+ "types": "./dist/memory-r2.d.ts",
82
+ "import": "./dist/memory-r2.js",
83
+ "default": "./dist/memory-r2.js"
84
+ },
85
+ "./r2": {
86
+ "types": "./dist/r2.d.ts",
87
+ "import": "./dist/r2.js",
88
+ "default": "./dist/r2.js"
89
+ },
90
+ "./types": {
91
+ "types": "./dist/types.d.ts",
92
+ "import": "./dist/types.js",
93
+ "default": "./dist/types.js"
94
+ }
95
+ },
96
+ "main": "./dist/index.js",
97
+ "types": "./dist/index.d.ts"
98
+ },
99
+ "dependencies": {
100
+ "@hono/node-server": "^2.0.6",
101
+ "@voyant-travel/storage": "workspace:^"
102
+ },
103
+ "devDependencies": {
104
+ "@types/node": "catalog:",
105
+ "@voyant-travel/voyant-typescript-config": "workspace:^",
106
+ "typescript": "catalog:",
107
+ "vitest": "catalog:"
108
+ },
109
+ "repository": {
110
+ "type": "git",
111
+ "url": "https://github.com/voyant-travel/voyant.git",
112
+ "directory": "packages/runtime"
113
+ }
114
+ }