@moku-labs/worker 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/dist/index.mjs ADDED
@@ -0,0 +1,491 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { a as defineDurableObject, c as coreConfig, d as stagePlugin, i as durableObjectsPlugin, l as createCore, n as queuesPlugin, o as d1Plugin, r as kvPlugin, s as bindingsPlugin, t as storagePlugin, u as createPlugin$1 } from "./storage-bL-U_fkA.mjs";
3
+ import { envPlugin, logPlugin } from "@moku-labs/common";
4
+ //#region src/plugins/server/api.ts
5
+ /**
6
+ * Builds the `app.server.*` surface the consumer's Worker default export reads.
7
+ *
8
+ * @param ctx - Plugin context: config, compiled state table, emit, require, has.
9
+ * @returns The server API object with `handle` and `scheduled` methods.
10
+ * @example
11
+ * ```typescript
12
+ * // Wired in index.ts as: api: ctx => createServerApi(ctx)
13
+ * const serverApi = createServerApi(ctx);
14
+ * const response = await serverApi.handle(request, env, exec);
15
+ * ```
16
+ */
17
+ const createServerApi = (ctx) => {
18
+ /**
19
+ * Route one HTTP request and return its `Response` (or 404 Not Found).
20
+ *
21
+ * Allocates a fresh `RequestContext` on the call stack carrying the
22
+ * per-request `env` — never stored on state (SB4). Response flows through
23
+ * the return value, not `emit` (F8; spec/07 §1).
24
+ *
25
+ * @param request - The incoming Cloudflare `Request`.
26
+ * @param env - Per-request Cloudflare bindings; threaded on the stack, never stored.
27
+ * @param exec - `ExecutionContext` for `waitUntil` / `passThroughOnException`.
28
+ * @returns The matched handler's `Response`, or `404 Not Found`.
29
+ * @example
30
+ * ```typescript
31
+ * const res = await serverApi.handle(new Request("https://example.com/"), env, exec);
32
+ * ```
33
+ */
34
+ const handle = async (request, env, exec) => {
35
+ const url = new URL(request.url);
36
+ const requestId = crypto.randomUUID();
37
+ const startTime = Date.now();
38
+ ctx.emit("request:start", {
39
+ method: request.method,
40
+ path: url.pathname,
41
+ requestId
42
+ });
43
+ const match = ctx.state.match(request.method, url.pathname);
44
+ if (!match) return new Response("Not Found", { status: 404 });
45
+ ctx.emit("server:matched", {
46
+ path: url.pathname,
47
+ method: request.method
48
+ });
49
+ const rc = {
50
+ request,
51
+ env,
52
+ exec,
53
+ params: match.params,
54
+ url,
55
+ require: ctx.require,
56
+ has: ctx.has
57
+ };
58
+ const response = await match.endpoint.handler(rc);
59
+ ctx.emit("request:end", {
60
+ method: request.method,
61
+ path: url.pathname,
62
+ status: response.status,
63
+ ms: Date.now() - startTime
64
+ });
65
+ return response;
66
+ };
67
+ /**
68
+ * Cron entry. Dispatches the `ScheduledController` through the same endpoint
69
+ * table as `handle` and **awaits** the matched handler so Cloudflare does not
70
+ * kill the isolate before the work finishes.
71
+ *
72
+ * Awaited API method — not `emit` — because the Worker must `await` cron work
73
+ * (F8; spec/07 §3). The `env` is threaded on the stack, never stored on state (SB4).
74
+ *
75
+ * @param controller - Cloudflare `ScheduledController` (`cron`, `scheduledTime`).
76
+ * @param env - Per-request Cloudflare bindings; threaded, never stored.
77
+ * @param exec - `ExecutionContext` for `waitUntil` / `passThroughOnException`.
78
+ * @returns Resolves after all matched cron work completes (or immediately if no match).
79
+ * @example
80
+ * ```typescript
81
+ * await serverApi.scheduled(controller, env, exec);
82
+ * ```
83
+ */
84
+ const scheduled = async (controller, env, exec) => {
85
+ const match = ctx.state.match("ALL", controller.cron);
86
+ if (!match) return;
87
+ const cronUrl = new URL(`https://cron/${controller.cron}`);
88
+ const rc = {
89
+ request: new Request(cronUrl.href),
90
+ env,
91
+ exec,
92
+ params: match.params,
93
+ url: cronUrl,
94
+ require: ctx.require,
95
+ has: ctx.has
96
+ };
97
+ await match.endpoint.handler(rc);
98
+ };
99
+ return {
100
+ handle,
101
+ scheduled
102
+ };
103
+ };
104
+ //#endregion
105
+ //#region src/plugins/server/helpers.ts
106
+ /**
107
+ * Produce an `Endpoint` value from a path, method, and handler.
108
+ *
109
+ * @param path - Endpoint path string.
110
+ * @param method - HTTP method literal or `"ALL"`.
111
+ * @param handler - The function invoked when this endpoint matches.
112
+ * @returns An `Endpoint` value object.
113
+ * @example
114
+ * ```typescript
115
+ * makeEndpoint("/api", "GET", handler); // { path: "/api", method: "GET", handler }
116
+ * ```
117
+ */
118
+ const makeEndpoint = (path, method, handler) => ({
119
+ path,
120
+ method,
121
+ handler
122
+ });
123
+ /**
124
+ * Build a typed `Endpoint`. `{name}` → required param; `{name?}` → optional param.
125
+ *
126
+ * PURE factory (spec/03 §1): no ctx, no lifecycle, no side effects; safe to run
127
+ * before `createApp`. Each verb method (`get`, `post`, …, `all`) returns the
128
+ * truthful Endpoint value — `method: "ALL"` is never used as a `"get"` sentinel.
129
+ *
130
+ * @param path - Endpoint path, optionally with `{name}` / `{name?}` params.
131
+ * @returns A builder whose verb methods each return a typed `Endpoint`.
132
+ * @example
133
+ * ```typescript
134
+ * endpoint("/api/data/{lang?}").get(({ params }) =>
135
+ * Response.json({ lang: params.lang ?? "en" })
136
+ * );
137
+ * ```
138
+ */
139
+ const endpoint = (path) => ({
140
+ /**
141
+ * Build a GET endpoint bound to this path.
142
+ *
143
+ * @param handler - The handler invoked when a GET request matches.
144
+ * @returns A GET `Endpoint`.
145
+ * @example
146
+ * ```typescript
147
+ * endpoint("/health").get(() => new Response("ok"));
148
+ * ```
149
+ */
150
+ get: (handler) => makeEndpoint(path, "GET", handler),
151
+ /**
152
+ * Build a POST endpoint bound to this path.
153
+ *
154
+ * @param handler - The handler invoked when a POST request matches.
155
+ * @returns A POST `Endpoint`.
156
+ * @example
157
+ * ```typescript
158
+ * endpoint("/users").post(({ request }) => Response.json({ created: true }, { status: 201 }));
159
+ * ```
160
+ */
161
+ post: (handler) => makeEndpoint(path, "POST", handler),
162
+ /**
163
+ * Build a PUT endpoint bound to this path.
164
+ *
165
+ * @param handler - The handler invoked when a PUT request matches.
166
+ * @returns A PUT `Endpoint`.
167
+ * @example
168
+ * ```typescript
169
+ * endpoint("/users/{id}").put(({ params }) => Response.json({ updated: params.id }));
170
+ * ```
171
+ */
172
+ put: (handler) => makeEndpoint(path, "PUT", handler),
173
+ /**
174
+ * Build a PATCH endpoint bound to this path.
175
+ *
176
+ * @param handler - The handler invoked when a PATCH request matches.
177
+ * @returns A PATCH `Endpoint`.
178
+ * @example
179
+ * ```typescript
180
+ * endpoint("/users/{id}").patch(({ params }) => Response.json({ patched: params.id }));
181
+ * ```
182
+ */
183
+ patch: (handler) => makeEndpoint(path, "PATCH", handler),
184
+ /**
185
+ * Build a DELETE endpoint bound to this path.
186
+ *
187
+ * @param handler - The handler invoked when a DELETE request matches.
188
+ * @returns A DELETE `Endpoint`.
189
+ * @example
190
+ * ```typescript
191
+ * endpoint("/users/{id}").delete(() => new Response(null, { status: 204 }));
192
+ * ```
193
+ */
194
+ delete: (handler) => makeEndpoint(path, "DELETE", handler),
195
+ /**
196
+ * Build a HEAD endpoint bound to this path.
197
+ *
198
+ * @param handler - The handler invoked when a HEAD request matches.
199
+ * @returns A HEAD `Endpoint`.
200
+ * @example
201
+ * ```typescript
202
+ * endpoint("/health").head(() => new Response(null, { status: 200 }));
203
+ * ```
204
+ */
205
+ head: (handler) => makeEndpoint(path, "HEAD", handler),
206
+ /**
207
+ * Build an OPTIONS endpoint bound to this path.
208
+ *
209
+ * @param handler - The handler invoked when an OPTIONS request matches.
210
+ * @returns An OPTIONS `Endpoint`.
211
+ * @example
212
+ * ```typescript
213
+ * endpoint("/api").options(() => new Response(null, { headers: { Allow: "GET, POST" } }));
214
+ * ```
215
+ */
216
+ options: (handler) => makeEndpoint(path, "OPTIONS", handler),
217
+ /**
218
+ * Build an ALL-method endpoint bound to this path (matches any verb).
219
+ *
220
+ * @param handler - The handler invoked when any request method matches.
221
+ * @returns An ALL-method `Endpoint`.
222
+ * @example
223
+ * ```typescript
224
+ * endpoint("0 * * * *").all(async () => new Response("cron done"));
225
+ * ```
226
+ */
227
+ all: (handler) => makeEndpoint(path, "ALL", handler)
228
+ });
229
+ //#endregion
230
+ //#region src/plugins/server/state.ts
231
+ /** Specificity weight for a literal path segment. */
232
+ const LITERAL_WEIGHT = 2;
233
+ /** Specificity weight for a required param segment `{name}`. */
234
+ const REQUIRED_PARAM_WEIGHT = 1;
235
+ /** Specificity weight for an optional param segment `{name?}`. */
236
+ const OPTIONAL_PARAM_WEIGHT = 0;
237
+ /**
238
+ * Parse one path segment string into a typed `PathSegment`.
239
+ *
240
+ * `{name}` → required param; `{name?}` → optional param; anything else → literal.
241
+ *
242
+ * @param raw - A single path segment token (no leading slash).
243
+ * @returns The parsed `PathSegment`.
244
+ * @example
245
+ * ```typescript
246
+ * parseSegment("{id}") // → { value: "id", param: true, optional: false }
247
+ * parseSegment("{id?}") // → { value: "id", param: true, optional: true }
248
+ * parseSegment("api") // → { value: "api", param: false, optional: false }
249
+ * ```
250
+ */
251
+ const parseSegment = (raw) => {
252
+ if (raw.startsWith("{") && raw.endsWith("}")) {
253
+ const inner = raw.slice(1, -1);
254
+ if (inner.endsWith("?")) return {
255
+ value: inner.slice(0, -1),
256
+ param: true,
257
+ optional: true
258
+ };
259
+ return {
260
+ value: inner,
261
+ param: true,
262
+ optional: false
263
+ };
264
+ }
265
+ return {
266
+ value: raw,
267
+ param: false,
268
+ optional: false
269
+ };
270
+ };
271
+ /**
272
+ * Compute the specificity weight for a single `PathSegment`.
273
+ *
274
+ * @param segment - The parsed segment to score.
275
+ * @returns `2` for literal, `1` for required param, `0` for optional param.
276
+ * @example
277
+ * ```typescript
278
+ * segmentWeight({ value: "api", param: false, optional: false }) // 2
279
+ * segmentWeight({ value: "id", param: true, optional: false }) // 1
280
+ * segmentWeight({ value: "q", param: true, optional: true }) // 0
281
+ * ```
282
+ */
283
+ const segmentWeight = (segment) => {
284
+ if (!segment.param) return LITERAL_WEIGHT;
285
+ if (!segment.optional) return REQUIRED_PARAM_WEIGHT;
286
+ return OPTIONAL_PARAM_WEIGHT;
287
+ };
288
+ /**
289
+ * Convert an `Endpoint` to its compiled form (parsed segments + specificity score).
290
+ *
291
+ * @param endpoint - The declarative endpoint value from config.
292
+ * @returns A `CompiledEndpoint` ready for the matcher table.
293
+ * @example
294
+ * ```typescript
295
+ * const compiled = compileEndpoint(endpoint("/api/{id}").get(handler));
296
+ * // compiled.specificity === 3 (literal "api" = 2, required "{id}" = 1)
297
+ * ```
298
+ */
299
+ const compileEndpoint = (endpoint) => {
300
+ const segments = endpoint.path.split("/").filter(Boolean).map((part) => parseSegment(part));
301
+ return {
302
+ endpoint,
303
+ segments,
304
+ specificity: segments.reduce((total, segment) => total + segmentWeight(segment), 0)
305
+ };
306
+ };
307
+ /**
308
+ * Try to match one compiled endpoint against a request method and split path tokens.
309
+ *
310
+ * Returns the extracted params map on success, or `undefined` if the endpoint
311
+ * does not match. Uses `undefined` (not `null`) per the unicorn/no-null rule.
312
+ *
313
+ * @param compiled - A single compiled endpoint.
314
+ * @param method - The request method string (e.g. `"GET"`).
315
+ * @param tokens - The request path split into non-empty segments.
316
+ * @returns Extracted params record on match, or `undefined` for no match.
317
+ * @example
318
+ * ```typescript
319
+ * const compiled = compileEndpoint(endpoint("/users/{id}").get(handler));
320
+ * tryMatchEndpoint(compiled, "GET", ["users", "42"]) // → { id: "42" }
321
+ * tryMatchEndpoint(compiled, "POST", ["users"]) // → undefined
322
+ * ```
323
+ */
324
+ const tryMatchEndpoint = (compiled, method, tokens) => {
325
+ if (compiled.endpoint.method !== "ALL" && compiled.endpoint.method !== method) return;
326
+ const { segments } = compiled;
327
+ const mandatoryCount = segments.filter((segment) => !segment.optional).length;
328
+ if (tokens.length < mandatoryCount || tokens.length > segments.length) return;
329
+ const params = {};
330
+ for (const [index, segment] of segments.entries()) {
331
+ const token = tokens[index];
332
+ if (segment.param) if (token === void 0) {
333
+ if (!segment.optional) return void 0;
334
+ params[segment.value] = void 0;
335
+ } else params[segment.value] = token;
336
+ else if (token !== segment.value) return;
337
+ }
338
+ return params;
339
+ };
340
+ /**
341
+ * Sort comparator placing higher-specificity endpoints first.
342
+ * Tie-break: method-specific endpoints before `ALL` so explicit methods win.
343
+ *
344
+ * @param a - First compiled endpoint.
345
+ * @param b - Second compiled endpoint.
346
+ * @returns Negative, zero, or positive sort key.
347
+ * @example
348
+ * ```typescript
349
+ * const a = compileEndpoint(endpoint("/api/{id}").get(handler)); // specificity 3
350
+ * const b = compileEndpoint(endpoint("/api/{id?}").get(handler)); // specificity 2
351
+ * [b, a].sort(bySpecificityDesc); // → [a, b] — higher specificity first
352
+ * ```
353
+ */
354
+ const bySpecificityDesc = (a, b) => {
355
+ const delta = b.specificity - a.specificity;
356
+ if (delta !== 0) return delta;
357
+ if (a.endpoint.method !== "ALL" && b.endpoint.method === "ALL") return -1;
358
+ if (a.endpoint.method === "ALL" && b.endpoint.method !== "ALL") return 1;
359
+ return 0;
360
+ };
361
+ /**
362
+ * Find the best-matching compiled endpoint in the table for the given method + path.
363
+ *
364
+ * Iterates the table (assumed sorted high-to-low specificity) and returns the
365
+ * first match. Internally re-sorts on every call so it is safe to call before
366
+ * `onInit` compiles the table.
367
+ *
368
+ * @param table - The compiled endpoint table.
369
+ * @param method - Request method string (e.g. `"GET"`, `"ALL"`).
370
+ * @param tokens - Path split into non-empty string tokens.
371
+ * @returns The match result, or `null` when no endpoint matches.
372
+ * @example
373
+ * ```typescript
374
+ * const result = findBestMatch(state.table, "GET", ["api", "users"]);
375
+ * ```
376
+ */
377
+ const findBestMatch = (table, method, tokens) => {
378
+ const sorted = table.toSorted(bySpecificityDesc);
379
+ for (const compiled of sorted) {
380
+ const params = tryMatchEndpoint(compiled, method, tokens);
381
+ if (params !== void 0) return {
382
+ endpoint: compiled.endpoint,
383
+ params
384
+ };
385
+ }
386
+ return null;
387
+ };
388
+ /**
389
+ * Compile and sort the endpoint table in-place.
390
+ *
391
+ * Called by `onInit` — the one-time per-isolate setup. Sorts `state.table` by
392
+ * specificity (descending), validates that no endpoint path contains duplicate
393
+ * `{param}` names, and sets `state.compiled = true` to guard re-entry.
394
+ *
395
+ * @param state - The mutable server state whose `table` should be compiled.
396
+ * @throws {Error} With `[moku-worker]` prefix when a path has duplicate param names.
397
+ * @example
398
+ * ```typescript
399
+ * // Called inside serverPlugin.onInit:
400
+ * compileServerState(ctx.state);
401
+ * ```
402
+ */
403
+ const compileServerState = (state) => {
404
+ if (state.compiled) return;
405
+ state.table.sort(bySpecificityDesc);
406
+ for (const compiled of state.table) {
407
+ const seen = /* @__PURE__ */ new Set();
408
+ for (const segment of compiled.segments) {
409
+ if (!segment.param) continue;
410
+ if (seen.has(segment.value)) throw new Error(`[moku-worker] endpoint path "${compiled.endpoint.path}" has duplicate param "{${segment.value}}".\n Each {param} name in a path must be unique.`);
411
+ seen.add(segment.value);
412
+ }
413
+ }
414
+ state.compiled = true;
415
+ };
416
+ /**
417
+ * Creates the initial (uncompiled) server state from a declarative endpoint list.
418
+ *
419
+ * Copies `endpoints` into a fresh mutable `CompiledEndpoint[]` — does NOT mutate
420
+ * the frozen config array. Sets `compiled = false`; `onInit` calls
421
+ * `compileServerState` to sort/validate and set `compiled = true`.
422
+ *
423
+ * The `match(method, path)` method is safe to call before `onInit` because
424
+ * `findBestMatch` re-sorts on every invocation.
425
+ *
426
+ * @param endpoints - The frozen declarative endpoint table from `config.endpoints`.
427
+ * @returns A fresh `ServerState` with `compiled = false`.
428
+ * @example
429
+ * ```typescript
430
+ * const state = createServerState(config.endpoints);
431
+ * const hit = state.match("GET", "/api/users");
432
+ * ```
433
+ */
434
+ const createServerState = (endpoints) => {
435
+ const table = endpoints.map((ep) => compileEndpoint(ep));
436
+ /**
437
+ * Match a method + pathname against the compiled table.
438
+ *
439
+ * @param method - Request method (or `"ALL"` for cron dispatch).
440
+ * @param path - Request URL pathname (or cron expression string).
441
+ * @returns Matched endpoint + extracted params, or `null` for no match.
442
+ * @example
443
+ * ```typescript
444
+ * state.match("GET", "/api/users");
445
+ * ```
446
+ */
447
+ const match = (method, path) => {
448
+ return findBestMatch(table, method, path.split("/").filter(Boolean));
449
+ };
450
+ return {
451
+ table,
452
+ compiled: false,
453
+ match
454
+ };
455
+ };
456
+ /**
457
+ * Standard tier — HTTP routing + request/scheduled dispatch over a compiled
458
+ * endpoint table. Emits `server:matched` (per-plugin) plus global
459
+ * `request:start` / `request:end` declared in `WorkerEvents`.
460
+ *
461
+ * @see README.md
462
+ */
463
+ const serverPlugin = createPlugin$1("server", {
464
+ events: (register) => register.map({ "server:matched": "An endpoint matched a request" }),
465
+ depends: [bindingsPlugin],
466
+ config: { endpoints: [] },
467
+ createState: ({ config }) => createServerState(config.endpoints),
468
+ api: (ctx) => createServerApi(ctx),
469
+ onInit: (ctx) => {
470
+ compileServerState(ctx.state);
471
+ },
472
+ helpers: { endpoint }
473
+ });
474
+ //#endregion
475
+ //#region src/plugins/d1/types.ts
476
+ var types_exports = /* @__PURE__ */ __exportAll({});
477
+ //#endregion
478
+ //#region src/plugins/durable-objects/types.ts
479
+ var types_exports$1 = /* @__PURE__ */ __exportAll({});
480
+ //#endregion
481
+ //#region src/plugins/queues/types.ts
482
+ var types_exports$2 = /* @__PURE__ */ __exportAll({});
483
+ //#endregion
484
+ //#region src/plugins/server/types.ts
485
+ var types_exports$3 = /* @__PURE__ */ __exportAll({});
486
+ //#endregion
487
+ //#region src/plugins/storage/types.ts
488
+ var types_exports$4 = /* @__PURE__ */ __exportAll({});
489
+ const { createApp, createPlugin } = createCore(coreConfig, { plugins: [bindingsPlugin, serverPlugin] });
490
+ //#endregion
491
+ export { types_exports as D1, types_exports$1 as DurableObjects, types_exports$2 as Queues, types_exports$3 as Server, types_exports$4 as Storage, bindingsPlugin, createApp, createPlugin, d1Plugin, defineDurableObject, durableObjectsPlugin, endpoint, envPlugin, kvPlugin, logPlugin, queuesPlugin, serverPlugin, stagePlugin, storagePlugin };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };