@max-cloud/functions 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maxemilian Grönblom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @max-cloud/functions
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Write functions for maxcloud, the homelab's run platform (Knative and KEDA,
4
+ see [maxemiliang/maxcloud](https://github.com/maxemiliang/maxcloud)). Export
5
+ them from your app's entry module, and each one is deployed on its own:
6
+
7
+ ```ts
8
+ import { enqueue, onQueue, onRequest, onSchedule } from "@max-cloud/functions";
9
+
10
+ // A Knative service: scales to zero, starts on the first request.
11
+ export const api = onRequest({ public: true }, async (req) => {
12
+ const { key } = await req.json();
13
+ await enqueue("images", { key });
14
+ return Response.json({ queued: key }, { status: 202 });
15
+ });
16
+
17
+ // A CronJob.
18
+ export const nightly = onSchedule("0 3 * * *", async () => {
19
+ // ...
20
+ });
21
+
22
+ // A worker that KEDA starts when the queue has jobs, and stops when it's empty.
23
+ export const resize = onQueue<{ key: string }>("images", async (job) => {
24
+ console.log("resizing", job.payload.key, "attempt", job.attempt);
25
+ });
26
+ ```
27
+
28
+ The export's name becomes the function's: `resizeImage` is deployed as
29
+ `resize-image`.
30
+
31
+ ## HTTP functions
32
+
33
+ `onRequest(handler)` or `onRequest(options, handler)`. The handler takes a
34
+ standard `Request` and returns a `Response`, so fetch-style frameworks work
35
+ as they are: `onRequest(app.fetch)` with Hono, for example.
36
+
37
+ | Option | Default | |
38
+ | --- | --- | --- |
39
+ | `public` | `false` | Reachable from the internet at `https://<name>-<app>.gronblom.dev`. Otherwise cluster-local. |
40
+ | `minInstances` | `0` | Instances kept running when idle. |
41
+ | `maxInstances` | `3` | |
42
+ | `concurrency` | `0` | Requests one instance handles before another starts (0: no limit). |
43
+ | `timeoutSeconds` | `300` | Per request. |
44
+
45
+ ## Scheduled functions
46
+
47
+ `onSchedule("0 3 * * *", handler)`, or `onSchedule(options, handler)` with
48
+ `schedule`, `timeZone` (default UTC), `retries` (default 0) and
49
+ `timeoutSeconds` (default 3600). A run that throws exits 1, and Kubernetes
50
+ retries it `retries` times.
51
+
52
+ ## Queues
53
+
54
+ `onQueue(queue, handler)` or `onQueue(options, handler)`. `enqueue(queue,
55
+ payload)` adds a job from anywhere in the app. Jobs live in the app's
56
+ Postgres database (`DATABASE_URL`), in a table the SDK creates
57
+ (`maxcloud_jobs`).
58
+
59
+ | Option | Default | |
60
+ | --- | --- | --- |
61
+ | `concurrency` | `1` | Jobs one instance works on at once. |
62
+ | `maxInstances` | `3` | |
63
+ | `maxAttempts` | `5` | Tries before a job is marked `failed`. |
64
+ | `backoffSeconds` | `10` | Delay before the first retry, doubled each time. |
65
+ | `timeoutSeconds` | `300` | Per job. A job past it is aborted and retried. |
66
+
67
+ - **Retries:** a job that throws is retried, and so is one whose worker
68
+ died, once its lease runs out. A job can run more than once, so make
69
+ handlers safe to repeat.
70
+ - **Enqueue in your own transaction:** pass `{ client }`, and the job exists
71
+ only if your transaction commits:
72
+ `await enqueue("mail", msg, { client: tx })`.
73
+ - **Delay a job** with `{ delaySeconds }`.
74
+
75
+ ## Running
76
+
77
+ The container runs `maxcloud-functions`, which starts the function named in
78
+ `FUNCTION_TARGET`:
79
+
80
+ - **HTTP:** a server on `$PORT`.
81
+ - **Schedule:** one run, then it exits.
82
+ - **Queue:** the worker loop.
83
+
84
+ On SIGTERM it finishes what's in flight and exits 0, so pods don't end in
85
+ `Error`. Your app's `package.json` needs:
86
+
87
+ ```json
88
+ {
89
+ "type": "module",
90
+ "main": "dist/index.js",
91
+ "scripts": { "start": "maxcloud-functions" }
92
+ }
93
+ ```
94
+
95
+ To try a function locally:
96
+
97
+ ```fish
98
+ FUNCTION_TARGET=api npx maxcloud-functions
99
+ ```
100
+
101
+ `maxcloud-functions --describe` prints the app's functions and their
102
+ settings as JSON; `mcloud` deploys from that. While it runs,
103
+ `MAXCLOUD_DESCRIBE=1` is set, so module-level code can skip connecting to
104
+ things.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ // maxcloud-functions: the container's entrypoint.
3
+ //
4
+ // maxcloud-functions run $FUNCTION_TARGET
5
+ // maxcloud-functions --target hello run that function
6
+ // maxcloud-functions --describe print the app's functions as JSON
7
+ // MAXCLOUD_DESCRIBE=1 maxcloud-functions the same, as one tagged line
8
+ import { parseArgs } from "node:util";
9
+ import { DESCRIBE_TAG, describe, load, run, sourcePath } from "./runtime.js";
10
+ const usage = `usage: maxcloud-functions [--target NAME] [--source FILE] [--port N]
11
+ maxcloud-functions --describe [--source FILE]
12
+
13
+ Runs one of the app's functions: the HTTP server, a scheduled run, or a queue
14
+ worker. NAME defaults to $FUNCTION_TARGET, FILE to package.json's "main", the
15
+ port to $PORT or 8080.`;
16
+ async function main() {
17
+ const { values } = parseArgs({
18
+ options: {
19
+ target: { type: "string" },
20
+ source: { type: "string" },
21
+ port: { type: "string" },
22
+ describe: { type: "boolean" },
23
+ help: { type: "boolean", short: "h" },
24
+ },
25
+ });
26
+ if (values.help) {
27
+ console.log(usage);
28
+ return 0;
29
+ }
30
+ // MAXCLOUD_DESCRIBE=1 in the environment does the same as --describe,
31
+ // as one tagged line: that's how the deploy workflow asks a built image,
32
+ // whatever its start command, and other output around it doesn't matter.
33
+ const fromEnv = !values.describe && process.env.MAXCLOUD_DESCRIBE === "1";
34
+ if (values.describe)
35
+ process.env.MAXCLOUD_DESCRIBE = "1";
36
+ const found = await load(sourcePath(values.source));
37
+ if (values.describe) {
38
+ process.stdout.write(JSON.stringify(describe(found), null, 2) + "\n");
39
+ return 0;
40
+ }
41
+ if (fromEnv) {
42
+ process.stdout.write(DESCRIBE_TAG + JSON.stringify(describe(found)) + "\n");
43
+ return 0;
44
+ }
45
+ const names = found.map((f) => f.name).join(", ") || "none";
46
+ const target = values.target ?? process.env.FUNCTION_TARGET;
47
+ if (!target) {
48
+ console.error(`maxcloud-functions: which function? Set FUNCTION_TARGET or --target (${names})`);
49
+ return 2;
50
+ }
51
+ const f = found.find((x) => x.name === target || x.exportName === target);
52
+ if (!f) {
53
+ console.error(`maxcloud-functions: no function ${target} (found: ${names})`);
54
+ return 2;
55
+ }
56
+ // SIGTERM is how Kubernetes stops a pod; finish cleanly and exit 0.
57
+ const stop = new AbortController();
58
+ for (const sig of ["SIGTERM", "SIGINT"]) {
59
+ process.once(sig, () => stop.abort(new Error(`${sig} received`)));
60
+ }
61
+ const port = Number(values.port ?? process.env.PORT ?? 8080);
62
+ return run(f, { port, signal: stop.signal });
63
+ }
64
+ main().then((code) => process.exit(code), (err) => {
65
+ console.error("maxcloud-functions:", err instanceof Error ? (err.stack ?? err.message) : err);
66
+ process.exit(1);
67
+ });
68
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /** Marks an export as a maxcloud function. */
2
+ export declare const FUNCTION: unique symbol;
3
+ /** Resources and limits every kind of function takes. */
4
+ export interface CommonOptions {
5
+ /** Memory the container requests and is limited to, e.g. "256Mi". */
6
+ memory?: string;
7
+ /** CPU the container requests, e.g. "100m". */
8
+ cpu?: string;
9
+ /**
10
+ * How long one invocation may run. HTTP: per request (default 300).
11
+ * Schedule: the whole run (default 3600). Queue: per job (default 300).
12
+ */
13
+ timeoutSeconds?: number;
14
+ }
15
+ export interface HttpOptions extends CommonOptions {
16
+ /** Reachable from the internet at https://<name>-<app>.gronblom.dev. Default false: cluster-local only. */
17
+ public?: boolean;
18
+ /** Instances kept running when idle. Default 0 (scale to zero). */
19
+ minInstances?: number;
20
+ /** Most instances at once. Default 3. */
21
+ maxInstances?: number;
22
+ /** Requests one instance handles at once before another starts. Default 0 (no limit; Knative scales on 100 in flight). */
23
+ concurrency?: number;
24
+ }
25
+ export interface ScheduleOptions extends CommonOptions {
26
+ /** Cron schedule, e.g. "0 3 * * *". */
27
+ schedule: string;
28
+ /** IANA time zone for the schedule. Default UTC. */
29
+ timeZone?: string;
30
+ /** Times a failed run is retried. Default 0. */
31
+ retries?: number;
32
+ }
33
+ export interface QueueOptions extends CommonOptions {
34
+ /** Queue name: lowercase letters, digits, dashes and underscores. */
35
+ queue: string;
36
+ /** Jobs one instance works on at once. Default 1. */
37
+ concurrency?: number;
38
+ /** Most instances at once. Default 3. */
39
+ maxInstances?: number;
40
+ /** Tries before a job is marked failed. Default 5. */
41
+ maxAttempts?: number;
42
+ /** Delay before the first retry, doubled for each later one. Default 10. */
43
+ backoffSeconds?: number;
44
+ }
45
+ export interface HttpContext {
46
+ /** Aborted when the client goes away or the server shuts down. */
47
+ signal: AbortSignal;
48
+ }
49
+ export type HttpHandler = (request: Request, context: HttpContext) => Response | Promise<Response>;
50
+ export interface ScheduleContext {
51
+ /** Aborted at the timeout, or when the run is stopped. */
52
+ signal: AbortSignal;
53
+ /** When this run started. */
54
+ startedAt: Date;
55
+ }
56
+ export type ScheduleHandler = (context: ScheduleContext) => void | Promise<void>;
57
+ export interface Job<T> {
58
+ id: string;
59
+ queue: string;
60
+ payload: T;
61
+ /** 1 on the first try. */
62
+ attempt: number;
63
+ createdAt: Date;
64
+ }
65
+ export interface JobContext {
66
+ /** Aborted at the timeout, or when the worker shuts down. */
67
+ signal: AbortSignal;
68
+ }
69
+ export type JobHandler<T> = (job: Job<T>, context: JobContext) => void | Promise<void>;
70
+ export interface HttpFunction {
71
+ readonly [FUNCTION]: true;
72
+ readonly kind: "http";
73
+ readonly options: HttpOptions;
74
+ readonly handler: HttpHandler;
75
+ /** Calls the handler directly, e.g. in tests. */
76
+ fetch(request: Request): Promise<Response>;
77
+ }
78
+ export interface ScheduleFunction {
79
+ readonly [FUNCTION]: true;
80
+ readonly kind: "schedule";
81
+ readonly options: ScheduleOptions;
82
+ readonly handler: ScheduleHandler;
83
+ }
84
+ export interface QueueFunction<T = unknown> {
85
+ readonly [FUNCTION]: true;
86
+ readonly kind: "queue";
87
+ readonly options: QueueOptions;
88
+ readonly handler: JobHandler<T>;
89
+ }
90
+ export type MaxcloudFunction = HttpFunction | ScheduleFunction | QueueFunction<any>;
91
+ export declare function isFunction(v: unknown): v is MaxcloudFunction;
92
+ /**
93
+ * An HTTP function: a Knative service that scales to zero.
94
+ *
95
+ * ```ts
96
+ * export const hello = onRequest(async (req) => new Response("hi"));
97
+ * export const api = onRequest({ public: true }, app.fetch); // e.g. a Hono app
98
+ * ```
99
+ */
100
+ export declare function onRequest(handler: HttpHandler): HttpFunction;
101
+ export declare function onRequest(options: HttpOptions, handler: HttpHandler): HttpFunction;
102
+ /**
103
+ * A scheduled function: a Kubernetes CronJob that runs it once per tick.
104
+ *
105
+ * ```ts
106
+ * export const nightly = onSchedule("0 3 * * *", async () => { ... });
107
+ * ```
108
+ */
109
+ export declare function onSchedule(schedule: string, handler: ScheduleHandler): ScheduleFunction;
110
+ export declare function onSchedule(options: ScheduleOptions, handler: ScheduleHandler): ScheduleFunction;
111
+ export declare function checkQueueName(queue: string): void;
112
+ /**
113
+ * A queue function: a worker that KEDA starts when the queue has jobs, and
114
+ * scales to zero when it's empty. Add jobs with `enqueue`.
115
+ *
116
+ * ```ts
117
+ * export const resize = onQueue<{ key: string }>("images", async (job) => { ... });
118
+ * ```
119
+ *
120
+ * A job that throws is retried with backoff; one still running when its
121
+ * timeout passes is aborted and retried too. Make handlers safe to run twice.
122
+ */
123
+ export declare function onQueue<T = unknown>(queue: string, handler: JobHandler<T>): QueueFunction<T>;
124
+ export declare function onQueue<T = unknown>(options: QueueOptions, handler: JobHandler<T>): QueueFunction<T>;
package/dist/define.js ADDED
@@ -0,0 +1,45 @@
1
+ // Defining functions: onRequest, onSchedule, onQueue. Each returns a plain
2
+ // object that the runtime recognises among the module's exports; nothing runs
3
+ // until the runtime picks one with FUNCTION_TARGET.
4
+ /** Marks an export as a maxcloud function. */
5
+ export const FUNCTION = Symbol.for("maxcloud.function");
6
+ export function isFunction(v) {
7
+ return typeof v === "object" && v !== null && FUNCTION in v;
8
+ }
9
+ export function onRequest(a, b) {
10
+ const [options, handler] = typeof a === "function" ? [{}, a] : [a, b];
11
+ if (typeof handler !== "function")
12
+ throw new TypeError("onRequest: missing handler");
13
+ return {
14
+ [FUNCTION]: true,
15
+ kind: "http",
16
+ options,
17
+ handler,
18
+ async fetch(request) {
19
+ return handler(request, { signal: request.signal });
20
+ },
21
+ };
22
+ }
23
+ export function onSchedule(a, handler) {
24
+ const options = typeof a === "string" ? { schedule: a } : a;
25
+ if (!options.schedule || options.schedule.trim().split(/\s+/).length !== 5) {
26
+ throw new TypeError(`onSchedule: ${JSON.stringify(options.schedule)} isn't a five-field cron schedule`);
27
+ }
28
+ if (typeof handler !== "function")
29
+ throw new TypeError("onSchedule: missing handler");
30
+ return { [FUNCTION]: true, kind: "schedule", options, handler };
31
+ }
32
+ const QUEUE_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
33
+ export function checkQueueName(queue) {
34
+ if (!QUEUE_NAME.test(queue)) {
35
+ throw new TypeError(`queue name ${JSON.stringify(queue)}: use lowercase letters, digits, dashes and underscores`);
36
+ }
37
+ }
38
+ export function onQueue(a, handler) {
39
+ const options = typeof a === "string" ? { queue: a } : a;
40
+ checkQueueName(options.queue);
41
+ if (typeof handler !== "function")
42
+ throw new TypeError("onQueue: missing handler");
43
+ return { [FUNCTION]: true, kind: "queue", options, handler };
44
+ }
45
+ //# sourceMappingURL=define.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,8EAA8E;AAC9E,oDAAoD;AAEpD,8CAA8C;AAC9C,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AAyGxD,MAAM,UAAU,UAAU,CAAC,CAAU;IACnC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,QAAQ,IAAI,CAAC,CAAC;AAC9D,CAAC;AAYD,MAAM,UAAU,SAAS,CAAC,CAA4B,EAAE,CAAe;IACrE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtE,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;IACrF,OAAO;QACL,CAAC,QAAQ,CAAC,EAAE,IAAI;QAChB,IAAI,EAAE,MAAM;QACZ,OAAO;QACP,OAAO;QACP,KAAK,CAAC,KAAK,CAAC,OAAO;YACjB,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC;AAWD,MAAM,UAAU,UAAU,CAAC,CAA2B,EAAE,OAAwB;IAC9E,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,SAAS,CAAC,eAAe,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,mCAAmC,CAAC,CAAC;IAC1G,CAAC;IACD,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,MAAM,IAAI,SAAS,CAAC,6BAA6B,CAAC,CAAC;IACtF,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,GAAG,4BAA4B,CAAC;AAEhD,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,SAAS,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;IACpH,CAAC;AACH,CAAC;AAeD,MAAM,UAAU,OAAO,CAAc,CAAwB,EAAE,OAAsB;IACnF,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,MAAM,IAAI,SAAS,CAAC,0BAA0B,CAAC,CAAC;IACnF,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC/D,CAAC"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type Server } from "node:http";
2
+ import type { HttpFunction } from "./define.ts";
3
+ export interface Serving {
4
+ server: Server;
5
+ /** The port it listens on (useful with port 0). */
6
+ port: number;
7
+ /** Stops accepting requests and resolves once those in flight have finished, or `graceMs` passed. */
8
+ close(graceMs?: number): Promise<void>;
9
+ }
10
+ export declare function serveHttp(fn: HttpFunction, port: number, log?: (msg: string) => void): Promise<Serving>;
package/dist/http.js ADDED
@@ -0,0 +1,106 @@
1
+ // Serving an HTTP function: Node's http server in front of a fetch-style
2
+ // handler (Request in, Response out).
3
+ import { createServer } from "node:http";
4
+ import { Readable } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
6
+ function toRequest(req, signal) {
7
+ // Traefik and Knative's queue-proxy set these for the original request.
8
+ const proto = String(req.headers["x-forwarded-proto"] ?? "http").split(",")[0].trim();
9
+ const host = req.headers["x-forwarded-host"] ?? req.headers.host ?? "localhost";
10
+ const url = new URL(req.url ?? "/", `${proto}://${host}`);
11
+ const headers = new Headers();
12
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
13
+ headers.append(req.rawHeaders[i], req.rawHeaders[i + 1]);
14
+ }
15
+ const method = req.method ?? "GET";
16
+ const hasBody = method !== "GET" && method !== "HEAD";
17
+ return new Request(url, {
18
+ method,
19
+ headers,
20
+ body: hasBody ? Readable.toWeb(req) : undefined,
21
+ signal,
22
+ // Node needs this for a streamed request body.
23
+ duplex: hasBody ? "half" : undefined,
24
+ });
25
+ }
26
+ async function writeResponse(res, response, head) {
27
+ res.statusCode = response.status;
28
+ if (response.statusText)
29
+ res.statusMessage = response.statusText;
30
+ const cookies = response.headers.getSetCookie();
31
+ response.headers.forEach((value, key) => {
32
+ if (key !== "set-cookie")
33
+ res.setHeader(key, value);
34
+ });
35
+ if (cookies.length)
36
+ res.setHeader("set-cookie", cookies);
37
+ if (!response.body || head) {
38
+ res.end();
39
+ return;
40
+ }
41
+ await pipeline(Readable.fromWeb(response.body), res);
42
+ }
43
+ export async function serveHttp(fn, port, log = console.log) {
44
+ const shutdown = new AbortController();
45
+ let inFlight = 0;
46
+ let idle;
47
+ const server = createServer(async (req, res) => {
48
+ inFlight++;
49
+ const client = new AbortController();
50
+ res.on("close", () => {
51
+ if (!res.writableFinished)
52
+ client.abort(new Error("client went away"));
53
+ });
54
+ const signal = AbortSignal.any([client.signal, shutdown.signal]);
55
+ try {
56
+ const response = await fn.handler(toRequest(req, signal), { signal });
57
+ if (!(response instanceof Response))
58
+ throw new TypeError("handler didn't return a Response");
59
+ await writeResponse(res, response, req.method === "HEAD");
60
+ }
61
+ catch (err) {
62
+ if (!client.signal.aborted) {
63
+ log(`${req.method} ${req.url}: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
64
+ }
65
+ if (!res.headersSent) {
66
+ res.statusCode = 500;
67
+ res.setHeader("content-type", "text/plain; charset=utf-8");
68
+ res.end("Internal Server Error\n");
69
+ }
70
+ else {
71
+ res.destroy();
72
+ }
73
+ }
74
+ finally {
75
+ inFlight--;
76
+ if (inFlight === 0)
77
+ idle?.();
78
+ }
79
+ });
80
+ // Knative's queue-proxy keeps connections open; let it reuse them a while.
81
+ server.keepAliveTimeout = 65_000;
82
+ await new Promise((resolve, reject) => {
83
+ server.once("error", reject);
84
+ server.listen(port, () => resolve());
85
+ });
86
+ const addr = server.address();
87
+ const actual = typeof addr === "object" && addr ? addr.port : port;
88
+ return {
89
+ server,
90
+ port: actual,
91
+ async close(graceMs = 25_000) {
92
+ const closed = new Promise((r) => server.close(() => r()));
93
+ server.closeIdleConnections();
94
+ if (inFlight > 0) {
95
+ await Promise.race([
96
+ new Promise((r) => (idle = r)),
97
+ new Promise((r) => setTimeout(r, graceMs).unref()),
98
+ ]);
99
+ }
100
+ shutdown.abort(new Error("server shutting down"));
101
+ server.closeAllConnections();
102
+ await closed;
103
+ },
104
+ };
105
+ }
106
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { onRequest, onSchedule, onQueue, type CommonOptions, type HttpOptions, type ScheduleOptions, type QueueOptions, type HttpHandler, type HttpContext, type ScheduleHandler, type ScheduleContext, type JobHandler, type JobContext, type Job, type HttpFunction, type ScheduleFunction, type QueueFunction, } from "./define.ts";
2
+ export { enqueue, type EnqueueOptions, type Queryable } from "./queue.ts";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { onRequest, onSchedule, onQueue, } from "./define.js";
2
+ export { enqueue } from "./queue.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,UAAU,EACV,OAAO,GAeR,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,EAAuC,MAAM,YAAY,CAAC"}
@@ -0,0 +1,59 @@
1
+ import pg from "pg";
2
+ import { type Job, type QueueFunction } from "./define.ts";
3
+ export declare const TABLE = "maxcloud_jobs";
4
+ /**
5
+ * The query KEDA scales a queue's worker on: jobs due or being worked on.
6
+ * Counting running jobs too keeps a worker alive until its jobs finish.
7
+ */
8
+ export declare function backlogQuery(queue: string): string;
9
+ /** Anything that can run a query: a Pool, or a client inside a transaction. */
10
+ export interface Queryable {
11
+ query: pg.Pool["query"];
12
+ }
13
+ /** The pool `enqueue` uses unless given a client. */
14
+ export declare function defaultPool(): pg.Pool;
15
+ /** Creates the table once per pool. Safe to race: the DDL runs under a lock. */
16
+ export declare function ensureSchema(db: pg.Pool): Promise<void>;
17
+ export interface EnqueueOptions {
18
+ /** Run no sooner than this many seconds from now. */
19
+ delaySeconds?: number;
20
+ /**
21
+ * Insert with this client, e.g. inside your own transaction, so the job
22
+ * exists only if the transaction commits. The table must already exist
23
+ * (any worker or earlier enqueue creates it).
24
+ */
25
+ client?: Queryable;
26
+ }
27
+ /**
28
+ * Adds a job to a queue and returns its id.
29
+ *
30
+ * ```ts
31
+ * await enqueue("images", { key: "uploads/cat.jpg" });
32
+ * ```
33
+ */
34
+ export declare function enqueue<T>(queue: string | QueueFunction<T>, payload: T, options?: EnqueueOptions): Promise<string>;
35
+ /** Claims the next due job, or one whose lease ran out. */
36
+ export declare function claim(db: Queryable, queue: string, leaseSeconds: number): Promise<Job<unknown> | undefined>;
37
+ export declare function complete(db: Queryable, id: string): Promise<void>;
38
+ /** Schedules a retry with exponential backoff, or marks the job failed after its last attempt. */
39
+ export declare function fail(db: Queryable, job: Job<unknown>, error: unknown, maxAttempts: number, backoffSeconds: number): Promise<"retry" | "failed">;
40
+ /** Puts a job back without counting the attempt, for a worker shutting down. */
41
+ export declare function release(db: Queryable, id: string): Promise<void>;
42
+ /** Deletes finished jobs older than the given number of days. */
43
+ export declare function prune(db: Queryable, days?: number): Promise<number>;
44
+ export interface WorkerOptions {
45
+ /** Defaults to a pool on DATABASE_URL. */
46
+ pool?: pg.Pool;
47
+ /** Stops the worker: no new jobs; jobs in hand finish, or are put back at `graceMs`. */
48
+ signal: AbortSignal;
49
+ /** How long jobs in hand get to finish after `signal`. Default 25 s. */
50
+ graceMs?: number;
51
+ /** How often an idle worker looks for jobs (it's also woken on enqueue). Default 2 s. */
52
+ pollMs?: number;
53
+ log?: (msg: string) => void;
54
+ }
55
+ /**
56
+ * Works on a queue until `signal` aborts. Resolves once every job in hand is
57
+ * done or put back.
58
+ */
59
+ export declare function runWorker(fn: QueueFunction<any>, opts: WorkerOptions): Promise<void>;
package/dist/queue.js ADDED
@@ -0,0 +1,214 @@
1
+ // The job queue: one Postgres table, maxcloud_jobs, in the app's database
2
+ // (DATABASE_URL). Workers claim rows with FOR UPDATE SKIP LOCKED, so two never
3
+ // take the same job, and hold them on a lease: a worker that dies leaves its
4
+ // job to be claimed again once the lease runs out.
5
+ import pg from "pg";
6
+ import { checkQueueName } from "./define.js";
7
+ export const TABLE = "maxcloud_jobs";
8
+ const CHANNEL = "maxcloud_jobs";
9
+ const SCHEMA = `
10
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
11
+ id bigserial PRIMARY KEY,
12
+ queue text NOT NULL,
13
+ payload jsonb NOT NULL DEFAULT '{}',
14
+ state text NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'running', 'done', 'failed')),
15
+ attempts integer NOT NULL DEFAULT 0,
16
+ run_after timestamptz NOT NULL DEFAULT now(),
17
+ locked_until timestamptz,
18
+ last_error text,
19
+ created_at timestamptz NOT NULL DEFAULT now(),
20
+ done_at timestamptz
21
+ );
22
+ CREATE INDEX IF NOT EXISTS ${TABLE}_open ON ${TABLE} (queue, run_after, id) WHERE state IN ('pending', 'running');
23
+ `;
24
+ /**
25
+ * The query KEDA scales a queue's worker on: jobs due or being worked on.
26
+ * Counting running jobs too keeps a worker alive until its jobs finish.
27
+ */
28
+ export function backlogQuery(queue) {
29
+ checkQueueName(queue);
30
+ return `SELECT count(*) FROM ${TABLE} WHERE queue = '${queue}' AND state IN ('pending', 'running') AND run_after <= now()`;
31
+ }
32
+ let pool;
33
+ function databaseUrl() {
34
+ const url = process.env.DATABASE_URL;
35
+ if (!url)
36
+ throw new Error("DATABASE_URL isn't set: queues need the app's Postgres database");
37
+ return url;
38
+ }
39
+ /** The pool `enqueue` uses unless given a client. */
40
+ export function defaultPool() {
41
+ pool ??= new pg.Pool({ connectionString: databaseUrl(), max: 4 });
42
+ return pool;
43
+ }
44
+ const ready = new WeakMap();
45
+ /** Creates the table once per pool. Safe to race: the DDL runs under a lock. */
46
+ export function ensureSchema(db) {
47
+ let p = ready.get(db);
48
+ if (!p) {
49
+ p = (async () => {
50
+ const client = await db.connect();
51
+ try {
52
+ await client.query("BEGIN");
53
+ await client.query(`SELECT pg_advisory_xact_lock(hashtext('${TABLE}'))`);
54
+ await client.query(SCHEMA);
55
+ await client.query("COMMIT");
56
+ }
57
+ catch (err) {
58
+ await client.query("ROLLBACK").catch(() => { });
59
+ throw err;
60
+ }
61
+ finally {
62
+ client.release();
63
+ }
64
+ })();
65
+ p.catch(() => ready.delete(db));
66
+ ready.set(db, p);
67
+ }
68
+ return p;
69
+ }
70
+ /**
71
+ * Adds a job to a queue and returns its id.
72
+ *
73
+ * ```ts
74
+ * await enqueue("images", { key: "uploads/cat.jpg" });
75
+ * ```
76
+ */
77
+ export async function enqueue(queue, payload, options = {}) {
78
+ const name = typeof queue === "string" ? queue : queue.options.queue;
79
+ checkQueueName(name);
80
+ let db = options.client;
81
+ if (!db) {
82
+ const p = defaultPool();
83
+ await ensureSchema(p);
84
+ db = p;
85
+ }
86
+ const delay = Math.max(0, options.delaySeconds ?? 0);
87
+ const res = await db.query(`INSERT INTO ${TABLE} (queue, payload, run_after) VALUES ($1, $2, now() + make_interval(secs => $3)) RETURNING id`, [name, JSON.stringify(payload ?? null), delay]);
88
+ await db.query(`SELECT pg_notify('${CHANNEL}', $1)`, [name]);
89
+ return res.rows[0].id;
90
+ }
91
+ /** Claims the next due job, or one whose lease ran out. */
92
+ export async function claim(db, queue, leaseSeconds) {
93
+ const res = await db.query(`UPDATE ${TABLE} j
94
+ SET state = 'running', attempts = j.attempts + 1, locked_until = now() + make_interval(secs => $2)
95
+ FROM (SELECT id FROM ${TABLE}
96
+ WHERE queue = $1 AND run_after <= now()
97
+ AND (state = 'pending' OR (state = 'running' AND locked_until < now()))
98
+ ORDER BY run_after, id
99
+ FOR UPDATE SKIP LOCKED
100
+ LIMIT 1) next
101
+ WHERE j.id = next.id
102
+ RETURNING j.id, j.queue, j.payload, j.attempts, j.created_at`, [queue, leaseSeconds]);
103
+ const r = res.rows[0];
104
+ if (!r)
105
+ return undefined;
106
+ return { id: r.id, queue: r.queue, payload: r.payload, attempt: r.attempts, createdAt: r.created_at };
107
+ }
108
+ export async function complete(db, id) {
109
+ await db.query(`UPDATE ${TABLE} SET state = 'done', done_at = now(), locked_until = NULL WHERE id = $1`, [id]);
110
+ }
111
+ /** Schedules a retry with exponential backoff, or marks the job failed after its last attempt. */
112
+ export async function fail(db, job, error, maxAttempts, backoffSeconds) {
113
+ const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
114
+ if (job.attempt >= maxAttempts) {
115
+ await db.query(`UPDATE ${TABLE} SET state = 'failed', done_at = now(), locked_until = NULL, last_error = $2 WHERE id = $1`, [job.id, message]);
116
+ return "failed";
117
+ }
118
+ const delay = backoffSeconds * 2 ** (job.attempt - 1);
119
+ await db.query(`UPDATE ${TABLE} SET state = 'pending', locked_until = NULL, last_error = $2, run_after = now() + make_interval(secs => $3) WHERE id = $1`, [job.id, message, delay]);
120
+ return "retry";
121
+ }
122
+ /** Puts a job back without counting the attempt, for a worker shutting down. */
123
+ export async function release(db, id) {
124
+ await db.query(`UPDATE ${TABLE} SET state = 'pending', attempts = greatest(attempts - 1, 0), locked_until = NULL WHERE id = $1`, [id]);
125
+ }
126
+ /** Deletes finished jobs older than the given number of days. */
127
+ export async function prune(db, days = 7) {
128
+ const res = await db.query(`DELETE FROM ${TABLE} WHERE state = 'done' AND done_at < now() - make_interval(days => $1)`, [days]);
129
+ return res.rowCount ?? 0;
130
+ }
131
+ /**
132
+ * Works on a queue until `signal` aborts. Resolves once every job in hand is
133
+ * done or put back.
134
+ */
135
+ export async function runWorker(fn, opts) {
136
+ const o = fn.options;
137
+ const concurrency = Math.max(1, o.concurrency ?? 1);
138
+ const timeoutMs = (o.timeoutSeconds ?? 300) * 1000;
139
+ const lease = Math.ceil(timeoutMs / 1000) + 30;
140
+ const maxAttempts = o.maxAttempts ?? 5;
141
+ const backoff = o.backoffSeconds ?? 10;
142
+ const log = opts.log ?? ((m) => console.log(m));
143
+ const db = opts.pool ?? new pg.Pool({ connectionString: databaseUrl(), max: concurrency + 1 });
144
+ const ownPool = !opts.pool;
145
+ await ensureSchema(db);
146
+ await prune(db).catch(() => { });
147
+ // Wake idle slots when a job is enqueued, instead of waiting for the poll.
148
+ let wake = () => { };
149
+ let wakeup = new Promise((r) => (wake = r));
150
+ const poke = () => {
151
+ wake();
152
+ wakeup = new Promise((r) => (wake = r));
153
+ };
154
+ const listener = await db.connect();
155
+ listener.on("notification", (n) => {
156
+ if (n.payload === o.queue)
157
+ poke();
158
+ });
159
+ await listener.query(`LISTEN ${CHANNEL}`);
160
+ opts.signal.addEventListener("abort", poke, { once: true });
161
+ const stopping = opts.signal;
162
+ const shutdown = new AbortController();
163
+ stopping.addEventListener("abort", () => setTimeout(() => shutdown.abort(), opts.graceMs ?? 25_000).unref(), { once: true });
164
+ const work = async (job) => {
165
+ const timeout = AbortSignal.timeout(timeoutMs);
166
+ const signal = AbortSignal.any([timeout, shutdown.signal]);
167
+ try {
168
+ await Promise.race([
169
+ fn.handler(job, { signal }),
170
+ new Promise((_, reject) => signal.addEventListener("abort", () => reject(signal.reason), { once: true })),
171
+ ]);
172
+ await complete(db, job.id);
173
+ log(`job ${job.id} done (attempt ${job.attempt})`);
174
+ }
175
+ catch (err) {
176
+ if (shutdown.signal.aborted && !timeout.aborted) {
177
+ await release(db, job.id);
178
+ log(`job ${job.id} put back: worker shutting down`);
179
+ return;
180
+ }
181
+ const reason = timeout.aborted ? new Error(`timed out after ${timeoutMs / 1000}s`) : err;
182
+ const outcome = await fail(db, job, reason, maxAttempts, backoff);
183
+ log(`job ${job.id} ${outcome === "failed" ? "failed for good" : "failed, will retry"} (attempt ${job.attempt}/${maxAttempts}): ${reason instanceof Error ? reason.message : String(reason)}`);
184
+ }
185
+ };
186
+ const slot = async () => {
187
+ while (!stopping.aborted) {
188
+ let job;
189
+ try {
190
+ job = await claim(db, o.queue, lease);
191
+ }
192
+ catch (err) {
193
+ log(`claiming from ${o.queue}: ${err instanceof Error ? err.message : String(err)}`);
194
+ }
195
+ if (job) {
196
+ await work(job);
197
+ continue;
198
+ }
199
+ await Promise.race([wakeup, new Promise((r) => setTimeout(r, opts.pollMs ?? 2000).unref())]);
200
+ }
201
+ };
202
+ log(`worker for queue ${o.queue} started (concurrency ${concurrency})`);
203
+ try {
204
+ await Promise.all(Array.from({ length: concurrency }, slot));
205
+ }
206
+ finally {
207
+ await listener.query(`UNLISTEN ${CHANNEL}`).catch(() => { });
208
+ listener.release();
209
+ if (ownPool)
210
+ await db.end();
211
+ }
212
+ log(`worker for queue ${o.queue} stopped`);
213
+ }
214
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { type MaxcloudFunction } from "./define.ts";
2
+ /** A Kubernetes-safe name for an export: resizeImage -> resize-image. */
3
+ export declare function kubeName(exportName: string): string;
4
+ export interface Found {
5
+ name: string;
6
+ exportName: string;
7
+ fn: MaxcloudFunction;
8
+ }
9
+ export declare function findFunctions(mod: Record<string, unknown>): Found[];
10
+ /** Prefixes the describe line printed when MAXCLOUD_DESCRIBE=1 is set. */
11
+ export declare const DESCRIBE_TAG = "maxcloud:describe:";
12
+ /** What `--describe` prints, with every default filled in. */
13
+ export declare function describe(found: Found[]): {
14
+ sdk: string;
15
+ version: string;
16
+ functions: ({
17
+ name: string;
18
+ export: string;
19
+ kind: "http" | "queue" | "schedule";
20
+ memory: string | null;
21
+ cpu: string | null;
22
+ public: boolean;
23
+ minInstances: number;
24
+ maxInstances: number;
25
+ concurrency: number;
26
+ timeoutSeconds: number;
27
+ } | {
28
+ name: string;
29
+ export: string;
30
+ kind: "http" | "queue" | "schedule";
31
+ memory: string | null;
32
+ cpu: string | null;
33
+ schedule: string;
34
+ timeZone: string;
35
+ retries: number;
36
+ timeoutSeconds: number;
37
+ } | {
38
+ name: string;
39
+ export: string;
40
+ kind: "http" | "queue" | "schedule";
41
+ memory: string | null;
42
+ cpu: string | null;
43
+ queue: string;
44
+ concurrency: number;
45
+ maxInstances: number;
46
+ maxAttempts: number;
47
+ backoffSeconds: number;
48
+ timeoutSeconds: number;
49
+ backlogQuery: string;
50
+ })[];
51
+ };
52
+ /** The app's entry module: --source, $FUNCTION_SOURCE, package.json's main, or index.js. */
53
+ export declare function sourcePath(source?: string, cwd?: string): string;
54
+ export declare function load(path: string): Promise<Found[]>;
55
+ export interface RunOptions {
56
+ port?: number;
57
+ /** Ends the run: HTTP drains, a worker stops, a schedule is aborted. */
58
+ signal: AbortSignal;
59
+ log?: (msg: string) => void;
60
+ }
61
+ /** Runs one function until it's done (schedule) or `signal` aborts. Resolves to the exit code. */
62
+ export declare function run(f: Found, opts: RunOptions): Promise<number>;
@@ -0,0 +1,131 @@
1
+ // The runtime: loads the app's module, finds its functions, and either
2
+ // describes them (for mcloud, which turns them into manifests) or runs one.
3
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
4
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
5
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
6
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
7
+ });
8
+ }
9
+ return path;
10
+ };
11
+ import { readFileSync, existsSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ import { pathToFileURL } from "node:url";
14
+ import { isFunction } from "./define.js";
15
+ import { serveHttp } from "./http.js";
16
+ import { backlogQuery, runWorker } from "./queue.js";
17
+ /** A Kubernetes-safe name for an export: resizeImage -> resize-image. */
18
+ export function kubeName(exportName) {
19
+ const name = exportName
20
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
21
+ .replace(/_/g, "-")
22
+ .toLowerCase();
23
+ if (!/^[a-z]([-a-z0-9]{0,38}[a-z0-9])?$/.test(name)) {
24
+ throw new Error(`export ${exportName}: its name (${name}) must start with a letter and be at most 40 letters, digits and dashes`);
25
+ }
26
+ return name;
27
+ }
28
+ export function findFunctions(mod) {
29
+ const out = [];
30
+ const names = new Set();
31
+ for (const [exportName, value] of Object.entries(mod)) {
32
+ if (!isFunction(value))
33
+ continue;
34
+ if (exportName === "default") {
35
+ throw new Error("a function is the default export; give it a named export instead, its name becomes the function's");
36
+ }
37
+ const name = kubeName(exportName);
38
+ if (names.has(name))
39
+ throw new Error(`two exports turn into the function name ${name}`);
40
+ names.add(name);
41
+ out.push({ name, exportName, fn: value });
42
+ }
43
+ return out.sort((a, b) => a.name.localeCompare(b.name));
44
+ }
45
+ /** Prefixes the describe line printed when MAXCLOUD_DESCRIBE=1 is set. */
46
+ export const DESCRIBE_TAG = "maxcloud:describe:";
47
+ /** What `--describe` prints, with every default filled in. */
48
+ export function describe(found) {
49
+ return {
50
+ sdk: "@max-cloud/functions",
51
+ version: packageVersion(),
52
+ functions: found.map(({ name, exportName, fn }) => {
53
+ const base = { name, export: exportName, kind: fn.kind, memory: fn.options.memory ?? null, cpu: fn.options.cpu ?? null };
54
+ switch (fn.kind) {
55
+ case "http": {
56
+ const o = fn.options;
57
+ return { ...base, public: o.public ?? false, minInstances: o.minInstances ?? 0, maxInstances: o.maxInstances ?? 3, concurrency: o.concurrency ?? 0, timeoutSeconds: o.timeoutSeconds ?? 300 };
58
+ }
59
+ case "schedule": {
60
+ const o = fn.options;
61
+ return { ...base, schedule: o.schedule, timeZone: o.timeZone ?? "UTC", retries: o.retries ?? 0, timeoutSeconds: o.timeoutSeconds ?? 3600 };
62
+ }
63
+ case "queue": {
64
+ const o = fn.options;
65
+ return { ...base, queue: o.queue, concurrency: o.concurrency ?? 1, maxInstances: o.maxInstances ?? 3, maxAttempts: o.maxAttempts ?? 5, backoffSeconds: o.backoffSeconds ?? 10, timeoutSeconds: o.timeoutSeconds ?? 300, backlogQuery: backlogQuery(o.queue) };
66
+ }
67
+ }
68
+ }),
69
+ };
70
+ }
71
+ function packageVersion() {
72
+ try {
73
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
74
+ }
75
+ catch {
76
+ return "unknown";
77
+ }
78
+ }
79
+ /** The app's entry module: --source, $FUNCTION_SOURCE, package.json's main, or index.js. */
80
+ export function sourcePath(source, cwd = process.cwd()) {
81
+ const s = source ?? process.env.FUNCTION_SOURCE;
82
+ if (s)
83
+ return resolve(cwd, s);
84
+ const pkgFile = resolve(cwd, "package.json");
85
+ if (existsSync(pkgFile)) {
86
+ const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
87
+ if (typeof pkg.main === "string")
88
+ return resolve(cwd, pkg.main);
89
+ }
90
+ return resolve(cwd, "index.js");
91
+ }
92
+ export async function load(path) {
93
+ const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(path).href));
94
+ return findFunctions(mod);
95
+ }
96
+ /** Runs one function until it's done (schedule) or `signal` aborts. Resolves to the exit code. */
97
+ export async function run(f, opts) {
98
+ const log = opts.log ?? ((m) => console.log(m));
99
+ const fn = f.fn;
100
+ switch (fn.kind) {
101
+ case "http": {
102
+ const serving = await serveHttp(fn, opts.port ?? 8080, log);
103
+ log(`${f.name}: listening on port ${serving.port}`);
104
+ await new Promise((r) => opts.signal.addEventListener("abort", () => r(), { once: true }));
105
+ log(`${f.name}: shutting down`);
106
+ await serving.close();
107
+ return 0;
108
+ }
109
+ case "schedule": {
110
+ const timeout = (fn.options.timeoutSeconds ?? 3600) * 1000;
111
+ const signal = AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]);
112
+ const startedAt = new Date();
113
+ try {
114
+ await Promise.race([
115
+ fn.handler({ signal, startedAt }),
116
+ new Promise((_, reject) => signal.addEventListener("abort", () => reject(signal.reason), { once: true })),
117
+ ]);
118
+ log(`${f.name}: done in ${((Date.now() - startedAt.getTime()) / 1000).toFixed(1)}s`);
119
+ return 0;
120
+ }
121
+ catch (err) {
122
+ log(`${f.name}: failed: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
123
+ return 1;
124
+ }
125
+ }
126
+ case "queue":
127
+ await runWorker(fn, { signal: opts.signal, log });
128
+ return 0;
129
+ }
130
+ }
131
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@max-cloud/functions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Write HTTP, scheduled and queue functions for maxcloud, the homelab's run platform",
5
+ "keywords": ["functions", "serverless", "knative", "keda"],
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/maxemiliang/maxcloud.git",
11
+ "directory": "packages/functions"
12
+ },
13
+ "engines": {
14
+ "node": ">=22"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "bin": {
23
+ "maxcloud-functions": "./dist/cli.js"
24
+ },
25
+ "files": ["dist"],
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "node --test --test-concurrency=1 \"src/**/*.test.ts\""
30
+ },
31
+ "dependencies": {
32
+ "pg": "^8.23.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^24.19.2",
36
+ "@types/pg": "^8.23.1",
37
+ "typescript": "^7.0.2"
38
+ }
39
+ }