@moku-labs/worker 0.5.0 → 0.6.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.
@@ -1,884 +0,0 @@
1
- import { envPlugin, logPlugin } from "@moku-labs/common";
2
- import { createCoreConfig, createCorePlugin } from "@moku-labs/core";
3
- /**
4
- * stage core plugin — deployment-stage / dev-mode detection, flat-injected on
5
- * every regular plugin's context as `ctx.stage` (spec/02 §6). No state, no
6
- * events, no depends, no lifecycle hooks.
7
- *
8
- * @see README.md
9
- * @example
10
- * ```typescript
11
- * // Inside any regular plugin's api factory:
12
- * api: (ctx) => ({
13
- * errorBody: (e: Error) =>
14
- * ctx.stage.isDev() ? e.stack ?? e.message : "Internal Error",
15
- * })
16
- * ```
17
- */
18
- const stagePlugin = createCorePlugin("stage", {
19
- config: { stage: "production" },
20
- /**
21
- * Builds the stage accessor surface from the resolved stage.
22
- *
23
- * @param ctx - Core plugin context (spec/02 §6 — `{ config, state }` only;
24
- * no `global`, `emit`, or `require`). `state` is unused by this plugin.
25
- * @param ctx.config - The resolved plugin config containing the deployment stage.
26
- * @returns The `ctx.stage` API: `isDev`, `isProduction`, `current`.
27
- * @example
28
- * ```typescript
29
- * const api = stagePlugin.spec.api({ config: { stage: "development" }, state: {} });
30
- * api.isDev(); // true
31
- * ```
32
- */
33
- api: ({ config }) => ({
34
- /**
35
- * Whether this Worker runs in the development stage.
36
- *
37
- * @returns True iff `stage === "development"`.
38
- * @example
39
- * ```typescript
40
- * if (ctx.stage.isDev()) return Response.json({ stack: err.stack });
41
- * ```
42
- */
43
- isDev: () => config.stage === "development",
44
- /**
45
- * Whether this Worker runs in the production stage. Note: false in "test".
46
- *
47
- * @returns True iff `stage === "production"`.
48
- * @example
49
- * ```typescript
50
- * const cc = ctx.stage.isProduction() ? "public, max-age=31536000" : "no-store";
51
- * ```
52
- */
53
- isProduction: () => config.stage === "production",
54
- /**
55
- * The raw deployment stage, as the literal union (not `string`).
56
- *
57
- * @returns The resolved stage.
58
- * @example
59
- * ```typescript
60
- * ctx.log.info("startup", { stage: ctx.stage.current() });
61
- * ```
62
- */
63
- current: () => config.stage
64
- })
65
- });
66
- const coreConfig = createCoreConfig("moku-worker", {
67
- config: {
68
- stage: "production",
69
- name: "moku-worker",
70
- compatibilityDate: ""
71
- },
72
- plugins: [
73
- logPlugin,
74
- envPlugin,
75
- stagePlugin
76
- ]
77
- });
78
- const { createPlugin, createCore } = coreConfig;
79
- //#endregion
80
- //#region src/plugins/bindings/api.ts
81
- /**
82
- * Checks whether a value read from an env object is nullish (null or undefined).
83
- * Cloudflare supplies either form when a binding is absent, so both must be caught.
84
- *
85
- * @param value - The value read from the env object.
86
- * @returns True when the value is null or undefined.
87
- * @example
88
- * ```typescript
89
- * isNullish(undefined); // true
90
- * isNullish(0); // false — falsy but bound
91
- * ```
92
- */
93
- const isNullish = (value) => value === void 0 || value === null;
94
- /**
95
- * Resolves binding `name` off a request-supplied env object, narrowed to T.
96
- * Throws a `[moku-worker]`-prefixed error when the binding is nullish.
97
- * The env argument is read but never retained.
98
- *
99
- * @param env - The Cloudflare request env object passed to fetch/scheduled/queue.
100
- * @param name - The binding name to resolve.
101
- * @returns The binding value narrowed to T.
102
- * @throws {Error} With a `[moku-worker]` prefix when the binding is null or undefined.
103
- * @example
104
- * ```typescript
105
- * const kv = requireBinding<KVNamespace>(env, "MY_KV");
106
- * ```
107
- */
108
- const requireBinding = (env, name) => {
109
- const value = env[name];
110
- if (isNullish(value)) throw new Error(`[moku-worker] binding "${name}" is not bound.\n Declare it in wrangler config and pass it in via the request env.`);
111
- return value;
112
- };
113
- /**
114
- * Returns true when `name` resolves to a non-nullish value on the request env.
115
- * Never throws. Use for optional-binding branching without forcing an error.
116
- *
117
- * @param env - The Cloudflare request env object passed to fetch/scheduled/queue.
118
- * @param name - The binding name to check.
119
- * @returns Whether the binding is present and non-nullish.
120
- * @example
121
- * ```typescript
122
- * const ok = hasBinding(env, "DB"); // false if DB is not bound
123
- * ```
124
- */
125
- const hasBinding = (env, name) => !isNullish(env[name]);
126
- /**
127
- * Builds the app.bindings API surface. The factory receives a context but does
128
- * not use it — bindings holds no state (F4) and all resolution is argument-local.
129
- *
130
- * @param _ctx - Plugin context (unused; bindings is stateless — F4).
131
- * @returns BindingsApi with `require` and `has` methods.
132
- * @example
133
- * ```typescript
134
- * const api = createBindingsApi(ctx);
135
- * const kv = api.require<KVNamespace>(env, "MY_KV");
136
- * ```
137
- */
138
- const createBindingsApi = (_ctx) => ({
139
- require: requireBinding,
140
- has: hasBinding
141
- });
142
- /**
143
- * Standard-tier stateless resolver — the binding-family dependency root.
144
- *
145
- * Exposes `require<T>(env, name)` and `has(env, name)` off a per-request env
146
- * object. Regular plugin so downstream binding plugins can declare
147
- * `depends: [bindingsPlugin]` and reach it via `ctx.require(bindingsPlugin)`.
148
- *
149
- * @see README.md
150
- */
151
- const bindingsPlugin = createPlugin("bindings", {
152
- config: { required: [] },
153
- api: createBindingsApi
154
- });
155
- //#endregion
156
- //#region src/plugins/d1/api.ts
157
- /**
158
- * Create the d1 api. Each method resolves the D1Database off the request
159
- * `env` via the bindings plugin, then forwards to the native D1 call. The
160
- * binding is never cached, so concurrent requests stay isolated (SB4).
161
- *
162
- * The return is intentionally NOT annotated `: Api`. Annotating it would
163
- * collapse the per-method call-site generic `<T>` on `query`/`first` to
164
- * `unknown`; instead the implementation forwards `<T>` to `all<T>()` /
165
- * `first<T>()` and `types.ts#Api` remains the public-surface source of truth.
166
- *
167
- * @param {D1Ctx} ctx - Plugin context (own config + require).
168
- * @returns {object} The d1 public api (query, first, run, batch, prepare, deployManifest).
169
- * @example
170
- * ```typescript
171
- * const api = createD1Api(ctx);
172
- * const { results } = await api.query<Product>(env, "SELECT * FROM products");
173
- * ```
174
- */
175
- const createD1Api = (ctx) => {
176
- const db = (env) => ctx.require(bindingsPlugin).require(env, ctx.config.binding);
177
- return {
178
- /**
179
- * Run a statement and return all rows. Forwards the call-site generic to
180
- * `all<T>()` so the result type is not widened to `unknown`.
181
- *
182
- * @param {WorkerEnv} env - Per-request Cloudflare bindings object.
183
- * @param {string} sql - SQL text with `?` placeholders.
184
- * @param {unknown[]} params - Bind parameters, in placeholder order.
185
- * @returns {Promise<D1Result<T>>} All rows (`.results` is `T[]`).
186
- * @example
187
- * ```typescript
188
- * const { results } = await api.query<Product>(env, "SELECT * FROM products WHERE active = ?", 1);
189
- * ```
190
- */
191
- query: (env, sql, ...params) => db(env).prepare(sql).bind(...params).all(),
192
- /**
193
- * Run a statement and return the first row, or `null` if there are none.
194
- * Forwards the call-site generic to `first<T>()`.
195
- *
196
- * @param {WorkerEnv} env - Per-request Cloudflare bindings object.
197
- * @param {string} sql - SQL text with `?` placeholders.
198
- * @param {unknown[]} params - Bind parameters, in placeholder order.
199
- * @returns {Promise<T | null>} The first row, or `null` if none matched.
200
- * @example
201
- * ```typescript
202
- * const row = await api.first<Product>(env, "SELECT * FROM products WHERE id = ?", id);
203
- * ```
204
- */
205
- first: (env, sql, ...params) => db(env).prepare(sql).bind(...params).first(),
206
- /**
207
- * Run a write/DDL statement (INSERT/UPDATE/DELETE/DDL) and return the
208
- * D1 result carrying `.meta` (e.g. `rows_written`, `last_row_id`).
209
- *
210
- * @param {WorkerEnv} env - Per-request Cloudflare bindings object.
211
- * @param {string} sql - SQL text with `?` placeholders.
212
- * @param {unknown[]} params - Bind parameters, in placeholder order.
213
- * @returns {Promise<D1Result>} Result carrying `.meta`.
214
- * @example
215
- * ```typescript
216
- * const res = await api.run(env, "INSERT INTO products (name) VALUES (?)", name);
217
- * const id = res.meta.last_row_id;
218
- * ```
219
- */
220
- run: (env, sql, ...params) => db(env).prepare(sql).bind(...params).run(),
221
- /**
222
- * Execute caller-built prepared statements atomically in one round-trip,
223
- * returning one result per statement in order.
224
- *
225
- * @param {WorkerEnv} env - Per-request Cloudflare bindings object.
226
- * @param {D1PreparedStatement[]} stmts - Statements built from prepare(env).
227
- * @returns {Promise<D1Result[]>} One result per statement, order preserved.
228
- * @example
229
- * ```typescript
230
- * const handle = api.prepare(env);
231
- * await api.batch(env, [handle.prepare("INSERT INTO a VALUES (1)").bind()]);
232
- * ```
233
- */
234
- batch: (env, stmts) => db(env).batch(stmts),
235
- /**
236
- * Resolve the request-scoped D1Database so callers can build prepared
237
- * statements for batch(). Issues no query itself.
238
- *
239
- * @param {WorkerEnv} env - Per-request Cloudflare bindings object.
240
- * @returns {D1Database} The request-resolved database handle.
241
- * @example
242
- * ```typescript
243
- * const handle = api.prepare(env);
244
- * const stmt = handle.prepare("SELECT * FROM t").bind();
245
- * ```
246
- */
247
- prepare: (env) => db(env),
248
- /**
249
- * Return this plugin's deploy metadata for the deploy plugin to read.
250
- * Build-time only — takes no `env`. The return is typed `DeployManifest`
251
- * (from types.ts), which pins `kind` to the literal `"d1"` without an
252
- * inline `as` assertion.
253
- *
254
- * @returns {DeployManifest} Deploy manifest entry `{ kind: "d1", binding, migrations }`.
255
- * @example
256
- * ```typescript
257
- * const m = api.deployManifest();
258
- * // => { kind: "d1", binding: "DB", migrations: "./migrations" }
259
- * ```
260
- */
261
- deployManifest: () => ({
262
- kind: "d1",
263
- binding: ctx.config.binding,
264
- migrations: ctx.config.migrations
265
- })
266
- };
267
- };
268
- /**
269
- * Standard tier — Cloudflare D1 SQL access (thin typed wrappers, not an ORM).
270
- *
271
- * Exposes `query`, `first`, `run`, `batch`, `prepare`, and `deployManifest`.
272
- * Resolves the D1 binding off the per-request `env` via the bindings plugin.
273
- * No state, no events, no lifecycle hooks (request-scoped, spec/06 §3).
274
- *
275
- * @see README.md
276
- */
277
- const d1Plugin = createPlugin("d1", {
278
- depends: [bindingsPlugin],
279
- config: {
280
- binding: "DB",
281
- migrations: ""
282
- },
283
- api: (ctx) => createD1Api(ctx)
284
- });
285
- //#endregion
286
- //#region src/plugins/durable-objects/api.ts
287
- /**
288
- * Builds the `app.durableObjects` API surface — `get` and `deployManifest`.
289
- *
290
- * All namespace resolution uses the per-call `env` argument: `env` is threaded, never
291
- * stored (SB4 / design §1a). The config bindings map is frozen and read-only. No state
292
- * is held on the plugin between calls (stateless — `Record<string, never>`).
293
- *
294
- * @param ctx - Plugin context with `config.bindings`, `require(bindingsPlugin)`, and core APIs.
295
- * @returns The durableObjects API: `{ get, deployManifest }`.
296
- * @example
297
- * ```typescript
298
- * const api = createDoApi(ctx);
299
- * const stub = api.get(env, "counter", "room-42");
300
- * const manifest = api.deployManifest(); // { kind: "do", bindings: { counter: "COUNTER" } }
301
- * ```
302
- */
303
- const createDoApi = (ctx) => ({
304
- /**
305
- * Resolves a `DurableObjectStub` off the per-request env.
306
- *
307
- * Maps `logicalName` → `config.bindings[logicalName]` (falling back to `logicalName`
308
- * itself when unmapped), derives a deterministic id via `namespace.idFromName(idName)`,
309
- * and returns the addressed stub. Synchronous — returns a stub, not a Promise.
310
- * Throws (via the bindings resolver) when the binding is not present on `env`.
311
- *
312
- * @param env - Per-request Cloudflare bindings object (Worker fetch/queue/scheduled env).
313
- * @param logicalName - Logical DO name used in code (e.g. `"counter"`).
314
- * @param idName - Stable id name passed to `idFromName` (e.g. `"room-42"`).
315
- * @returns The addressed `DurableObjectStub`.
316
- * @throws {Error} With `[moku-worker]` prefix when the binding is not bound on `env`.
317
- * @example
318
- * ```typescript
319
- * const stub = app.durableObjects.get(env, "counter", "room-42");
320
- * const res = await stub.fetch("https://do/increment");
321
- * ```
322
- */
323
- get: (env, logicalName, idName) => {
324
- const binding = ctx.config.bindings[logicalName] ?? logicalName;
325
- const ns = ctx.require(bindingsPlugin).require(env, binding);
326
- return ns.get(ns.idFromName(idName));
327
- },
328
- /**
329
- * Returns this plugin's deploy metadata — read by the `deploy` plugin via
330
- * `ctx.require(durableObjectsPlugin)`. Never reads sibling `pluginConfigs` (F6;
331
- * spec/08 §5, §7). Pure synchronous read of `ctx.config.bindings`.
332
- *
333
- * @returns `{ kind: "do", bindings }` reflecting the frozen plugin config.
334
- * @example
335
- * ```typescript
336
- * const manifest = app.durableObjects.deployManifest();
337
- * // → { kind: "do", bindings: { counter: "COUNTER" } }
338
- * ```
339
- */
340
- deployManifest: () => ({
341
- kind: "do",
342
- bindings: ctx.config.bindings
343
- })
344
- });
345
- //#endregion
346
- //#region src/plugins/durable-objects/helpers.ts
347
- /**
348
- * Returns a base class the consumer extends and exports from `worker.ts`.
349
- *
350
- * PURE (spec/03 §1): takes no `ctx`, has no side effects, and may be called before
351
- * `createApp`. The static `doName` property captures `name` for diagnostics and
352
- * binding correlation. The constructor stores `(state, env)` as `this.ctx` / `this.env`,
353
- * satisfying the Cloudflare Durable Object constructor contract. The plugin NEVER
354
- * generates the final exported class — the consumer owns that class.
355
- *
356
- * @param name - Logical DO name; captured as `static doName` for diagnostics.
357
- * @returns A base class (constructor) the consumer extends.
358
- * @example
359
- * ```typescript
360
- * // src/counter.ts
361
- * import { defineDurableObject } from "@moku-labs/worker";
362
- *
363
- * export class Counter extends defineDurableObject("Counter") {
364
- * async fetch(): Promise<Response> {
365
- * const n = ((await this.ctx.storage.get<number>("n")) ?? 0) + 1;
366
- * await this.ctx.storage.put("n", n);
367
- * return Response.json({ n });
368
- * }
369
- * }
370
- * ```
371
- */
372
- const defineDurableObject = (name) => {
373
- /**
374
- * Base implementation of the Cloudflare Durable Object constructor contract.
375
- * Stores `(ctx, env)` as readonly properties for consumer subclasses to use.
376
- */
377
- class DurableObjectBaseImpl {
378
- /**
379
- * Cloudflare per-object storage/alarm context (DurableObjectState).
380
- * Use `this.ctx.storage` to read/write durable storage and `this.ctx.id` to inspect the DO id.
381
- */
382
- ctx;
383
- /**
384
- * Per-object Cloudflare bindings (per-request WorkerEnv).
385
- * Mirrors the env passed at construction time; never cached across requests.
386
- */
387
- env;
388
- /**
389
- * Logical DO name captured from `defineDurableObject(name)`.
390
- * Used for diagnostics and binding correlation.
391
- */
392
- static doName = name;
393
- /**
394
- * Constructs the base Durable Object with Cloudflare's required signature.
395
- *
396
- * @param ctx - Cloudflare DurableObjectState (storage, id, blockConcurrencyWhile, …).
397
- * @param env - Per-request Cloudflare bindings object (WorkerEnv).
398
- * @example
399
- * ```typescript
400
- * class Counter extends Base {
401
- * constructor(ctx: DurableObjectState, env: WorkerEnv) { super(ctx, env); }
402
- * }
403
- * ```
404
- */
405
- constructor(ctx, env) {
406
- this.ctx = ctx;
407
- this.env = env;
408
- }
409
- }
410
- return DurableObjectBaseImpl;
411
- };
412
- /**
413
- * Cloudflare Durable Objects plugin — Standard tier.
414
- *
415
- * Exposes `get(env, logicalName, idName)` (synchronous stub accessor, threaded env) and
416
- * `deployManifest()` (build-time metadata). Depends on `bindingsPlugin` for namespace
417
- * resolution. The `defineDurableObject` helper is mounted under `helpers` and re-exported
418
- * at the top level for consumer use.
419
- *
420
- * @example
421
- * ```typescript
422
- * // Consumer endpoint handler:
423
- * const stub = app.durableObjects.get(env, "counter", params.room!);
424
- * const res = await stub.fetch("https://do/increment");
425
- * // Consumer DO class:
426
- * export class Counter extends defineDurableObject("Counter") {
427
- * async fetch(): Promise<Response> { return new Response("ok"); }
428
- * }
429
- * ```
430
- * @see README.md
431
- */
432
- const durableObjectsPlugin = createPlugin("durableObjects", {
433
- depends: [bindingsPlugin],
434
- config: { bindings: {} },
435
- api: createDoApi,
436
- helpers: { defineDurableObject }
437
- });
438
- //#endregion
439
- //#region src/plugins/kv/api.ts
440
- /**
441
- * Builds the app.kv.* api. Resolves the KV namespace off the REQUEST-SUPPLIED env
442
- * on every call — env is threaded, never stored (design §1a / SB4).
443
- *
444
- * @param ctx - The kv plugin context (own config + merged events).
445
- * @returns The app.kv api: get / put / delete / list / deployManifest.
446
- * @example
447
- * ```typescript
448
- * const api = createKvApi(ctx);
449
- * const value = await api.get(env, "key");
450
- * ```
451
- */
452
- const createKvApi = (ctx) => {
453
- const ns = (env) => ctx.require(bindingsPlugin).require(env, ctx.config.binding);
454
- return {
455
- /**
456
- * Reads a value by key from the KV namespace. Returns null when absent.
457
- *
458
- * @param env - The per-request Cloudflare env (threaded, never stored).
459
- * @param key - The key to read.
460
- * @returns The stored value, or null when absent.
461
- * @example
462
- * ```typescript
463
- * const value = await api.get(env, "feature-flags");
464
- * ```
465
- */
466
- get: async (env, key) => ns(env).get(key),
467
- /**
468
- * Writes a string value under a key, optionally with KV put options.
469
- *
470
- * @param env - The per-request Cloudflare env.
471
- * @param key - The key to write.
472
- * @param value - The string value to store.
473
- * @param opts - Optional expiration / metadata.
474
- * @returns Resolves once the write is acknowledged.
475
- * @example
476
- * ```typescript
477
- * await api.put(env, "session:1", "data", { expirationTtl: 3600 });
478
- * ```
479
- */
480
- put: async (env, key, value, opts) => ns(env).put(key, value, opts),
481
- /**
482
- * Removes a key from the namespace (no-op if absent).
483
- *
484
- * @param env - The per-request Cloudflare env.
485
- * @param key - The key to delete.
486
- * @returns Resolves once the delete is acknowledged.
487
- * @example
488
- * ```typescript
489
- * await api.delete(env, "session:expired");
490
- * ```
491
- */
492
- delete: async (env, key) => ns(env).delete(key),
493
- /**
494
- * Lists keys in the namespace, optionally filtered/paginated via opts.
495
- *
496
- * @param env - The per-request Cloudflare env.
497
- * @param opts - Optional prefix / cursor / limit.
498
- * @returns The list result from the KV namespace.
499
- * @example
500
- * ```typescript
501
- * const { keys } = await api.list(env, { prefix: "session:" });
502
- * ```
503
- */
504
- list: async (env, opts) => ns(env).list(opts),
505
- /**
506
- * Returns this plugin's own deploy metadata, read by the deploy plugin via
507
- * require (design §6 / F6). Build-time only — takes no env.
508
- *
509
- * @returns The kv deploy descriptor with kind literal and binding name.
510
- * @example
511
- * ```typescript
512
- * const manifest = api.deployManifest(); // { kind: "kv", binding: "KV" }
513
- * ```
514
- */
515
- deployManifest: () => ({
516
- kind: "kv",
517
- binding: ctx.config.binding
518
- })
519
- };
520
- };
521
- /**
522
- * Micro tier — thin env-first wrapper over a Cloudflare KV namespace.
523
- *
524
- * Resolves the KV namespace per request via `ctx.require(bindingsPlugin)`;
525
- * never stores env in state (design §1a / SB4). No lifecycle hooks —
526
- * request-scoped; nothing to open or close.
527
- *
528
- * @see README.md
529
- */
530
- const kvPlugin = createPlugin("kv", {
531
- depends: [bindingsPlugin],
532
- config: { binding: "KV" },
533
- api: createKvApi
534
- });
535
- //#endregion
536
- //#region src/plugins/queues/api.ts
537
- /**
538
- * @file queues plugin — API factory (send, sendBatch, consume, deployManifest).
539
- *
540
- * All binding-resolving methods take the per-request `env` as the first argument
541
- * and resolve the `Queue` via `ctx.require(bindingsPlugin).require<Queue>(env, name)`.
542
- * The `env` is never stored (SB4 / design §1a) — resolved fresh on every call.
543
- */
544
- /**
545
- * Builds app.queues.* — read by worker.ts queue() delegation (design §1d; spec/02 §7).
546
- *
547
- * Resolves Queue bindings off the request env per call (never stored — SB4).
548
- * Emits `queue:message` for observability after each consumed message (F8).
549
- *
550
- * @param ctx - Plugin context (own config + require + emit).
551
- * @returns The queues API surface: send, sendBatch, consume, deployManifest.
552
- * @example
553
- * ```ts
554
- * // Worker entry (design §1d)
555
- * export default {
556
- * queue: (b, e, c) => app.queues.consume(b, e, c),
557
- * };
558
- * ```
559
- */
560
- const createQueuesApi = (ctx) => {
561
- /**
562
- * Resolves a named Queue binding from the per-request env.
563
- * Throws a [moku-worker]-prefixed error when the binding is absent.
564
- *
565
- * @param env - Per-request Cloudflare bindings.
566
- * @param name - Queue binding name.
567
- * @returns The resolved Queue instance.
568
- * @example
569
- * ```ts
570
- * const q = queue(env, "ORDERS");
571
- * ```
572
- */
573
- const queue = (env, name) => ctx.require(bindingsPlugin).require(env, name);
574
- return {
575
- /**
576
- * Enqueue a single message onto the named queue.
577
- *
578
- * Resolves the Queue binding fresh from `env` on every call (SB4).
579
- * Request/response work → api method, never emit (F8).
580
- *
581
- * @param env - Per-request Cloudflare bindings object.
582
- * @param q - Target queue binding name in `env`.
583
- * @param body - Message body to enqueue.
584
- * @returns Resolves once the message is enqueued.
585
- * @throws {Error} With a `[moku-worker]` prefix if the binding is missing.
586
- * @example
587
- * ```ts
588
- * await app.queues.send(env, "ORDERS", { orderId: "123" });
589
- * ```
590
- */
591
- send: async (env, q, body) => {
592
- await queue(env, q).send(body);
593
- },
594
- /**
595
- * Enqueue many messages in one call; each element becomes one message.
596
- *
597
- * Maps each body to `{ body }` before calling `Queue.sendBatch` (design §4.3).
598
- *
599
- * @param env - Per-request Cloudflare bindings object.
600
- * @param q - Target queue binding name in `env`.
601
- * @param bodies - Array of message bodies; each becomes one message.
602
- * @returns Resolves once all messages are enqueued.
603
- * @throws {Error} With a `[moku-worker]` prefix if the binding is missing.
604
- * @example
605
- * ```ts
606
- * await app.queues.sendBatch(env, "ORDERS", orders);
607
- * ```
608
- */
609
- sendBatch: async (env, q, bodies) => {
610
- await queue(env, q).sendBatch(bodies.map((body) => ({ body })));
611
- },
612
- /**
613
- * Consumer dispatch — the Worker's `queue()` export delegates here.
614
- *
615
- * Iterates `batch.messages`, **awaits** `config.onMessage(message, env)` per message
616
- * (so Cloudflare gets a settled promise and the handler controls ack/retry; F8,
617
- * spec/07 §3 — never emit for awaited work), then fire-and-forget emits `queue:message`
618
- * for observability. Returns a promise the Worker **must** await so the isolate is not
619
- * killed mid-batch.
620
- *
621
- * @param batch - The incoming message batch from Cloudflare.
622
- * @param env - Per-request Cloudflare bindings object.
623
- * @param _exec - waitUntil / passThroughOnException (reserved for future use).
624
- * @returns Resolves after all messages in the batch are processed.
625
- * @throws {Error} Re-throws any error from `config.onMessage` so Cloudflare can retry.
626
- * @example
627
- * ```ts
628
- * // Worker entry
629
- * queue: (b, e, c) => app.queues.consume(b, e, c),
630
- * ```
631
- */
632
- consume: async (batch, env, _exec) => {
633
- for (const m of batch.messages) {
634
- await ctx.config.onMessage(m, env);
635
- ctx.emit("queue:message", {
636
- queue: batch.queue,
637
- messageId: m.id
638
- });
639
- }
640
- },
641
- /**
642
- * Returns this plugin's deploy metadata, read by the deploy plugin via
643
- * `ctx.require(queuesPlugin).deployManifest()` (F6 — never reads sibling config).
644
- *
645
- * @returns Deploy manifest entry `{ kind: "queue", producers }`.
646
- * @example
647
- * ```ts
648
- * const manifest = ctx.require(queuesPlugin).deployManifest();
649
- * // → { kind: "queue", producers: ["orders"] }
650
- * ```
651
- */
652
- deployManifest: () => ({
653
- kind: "queue",
654
- producers: ctx.config.producers
655
- })
656
- };
657
- };
658
- /**
659
- * Standard tier — Cloudflare Queues producer + consumer dispatch.
660
- *
661
- * `events` is declared first and via `register.map<QueueEvents>` so the plugin's own events infer
662
- * into the factory context; the api wiring is therefore arrow-wrapped (contextually typed).
663
- *
664
- * Emits the plugin-local `queue:message` event after each consumed message.
665
- *
666
- * @see README.md
667
- */
668
- const queuesPlugin = createPlugin("queues", {
669
- events: (register) => register.map({ "queue:message": "A queue message was processed" }),
670
- depends: [bindingsPlugin],
671
- config: {
672
- producers: [],
673
- onMessage: async () => {}
674
- },
675
- api: (ctx) => createQueuesApi(ctx)
676
- });
677
- //#endregion
678
- //#region src/plugins/storage/providers/r2.ts
679
- /**
680
- * Build a StorageProvider backed by the real R2Bucket resolved off the
681
- * per-request env via the bindings plugin. The bucket is resolved fresh on
682
- * EVERY method call — never cached, so concurrent requests stay isolated
683
- * (worker-api-design SB4; spec/08 §6).
684
- *
685
- * Each method is `async` so that synchronous throws from `bindings.require`
686
- * (e.g. missing binding) are automatically wrapped in rejected Promises —
687
- * callers can always use `await` / `.catch` instead of `try/catch`.
688
- *
689
- * @param bindings - The bindings plugin API (provides `require<T>`).
690
- * @param env - The per-request Cloudflare bindings object.
691
- * @param bucket - The R2 bucket binding name (e.g. "ASSETS").
692
- * @returns {StorageProvider} A provider that delegates to the resolved R2Bucket.
693
- * @example
694
- * ```typescript
695
- * const provider = resolveR2Provider(ctx.require(bindingsPlugin), env, ctx.config.bucket);
696
- * const body = await provider.get("my-object");
697
- * ```
698
- */
699
- const resolveR2Provider = (bindings, env, bucket) => {
700
- /**
701
- * Resolve the R2Bucket for this request's env. Throws on missing binding.
702
- *
703
- * @returns {R2Bucket} The resolved R2Bucket binding.
704
- * @example
705
- * ```typescript
706
- * const bucket = b();
707
- * ```
708
- */
709
- const b = () => bindings.require(env, bucket);
710
- return {
711
- /**
712
- * Read an object from the bucket.
713
- *
714
- * @param key - The object key.
715
- * @returns {Promise<R2ObjectBody | null>} The R2ObjectBody, or null if the key is absent.
716
- * @example
717
- * ```typescript
718
- * const body = await provider.get("assets/logo.png");
719
- * ```
720
- */
721
- async get(key) {
722
- return b().get(key);
723
- },
724
- /**
725
- * Write an object to the bucket.
726
- *
727
- * @param key - The object key.
728
- * @param value - The object contents (any R2-accepted type).
729
- * @returns {Promise<R2Object>} The R2Object metadata for the written object.
730
- * @example
731
- * ```typescript
732
- * const obj = await provider.put("assets/logo.png", buffer);
733
- * ```
734
- */
735
- async put(key, value) {
736
- return b().put(key, value);
737
- },
738
- /**
739
- * Remove one or more objects from the bucket. No-op when a key is absent.
740
- *
741
- * @param key - A single key or array of keys to remove.
742
- * @returns {Promise<void>} Resolves once removed.
743
- * @example
744
- * ```typescript
745
- * await provider.delete("assets/old.png");
746
- * ```
747
- */
748
- async delete(key) {
749
- return b().delete(key);
750
- },
751
- /**
752
- * List objects, optionally filtered by R2ListOptions.
753
- *
754
- * @param opts - Optional list options (prefix, limit, cursor, delimiter).
755
- * @returns {Promise<R2Objects>} The R2Objects list result.
756
- * @example
757
- * ```typescript
758
- * const { objects } = await provider.list({ prefix: "images/" });
759
- * ```
760
- */
761
- async list(opts) {
762
- return b().list(opts);
763
- }
764
- };
765
- };
766
- //#endregion
767
- //#region src/plugins/storage/api.ts
768
- /**
769
- * Build the env-first storage API. Each runtime method resolves the bucket
770
- * provider fresh from the per-request `env` — nothing is stored, so concurrent
771
- * requests stay isolated (worker-api-design SB4; spec/08 §6,§7).
772
- *
773
- * The `deployManifest()` method is build-time only: it reads from `ctx.config`
774
- * and never touches `env` or R2.
775
- *
776
- * @param ctx - Plugin context (config + require for bindings resolution).
777
- * @returns {StorageApi} The env-first storage API surface.
778
- * @example
779
- * ```typescript
780
- * const api = createStorageApi(ctx);
781
- * const body = await api.get(env, "my-object");
782
- * ```
783
- */
784
- const createStorageApi = (ctx) => {
785
- /**
786
- * Resolve the StorageProvider for the given per-request env. Called on every
787
- * method invocation — the bucket binding is never cached across calls.
788
- *
789
- * @param env - The per-request Cloudflare bindings object.
790
- * @returns {StorageProvider} A StorageProvider delegating to the resolved R2Bucket.
791
- * @example
792
- * ```typescript
793
- * const p = provider(env);
794
- * const body = await p.get("key");
795
- * ```
796
- */
797
- const provider = (env) => resolveR2Provider(ctx.require(bindingsPlugin), env, ctx.config.bucket);
798
- return {
799
- /**
800
- * Read an object from the bucket; resolves null when the key is absent.
801
- *
802
- * @param env - Per-request Cloudflare bindings.
803
- * @param key - Object key.
804
- * @returns {Promise<R2ObjectBody | null>} The R2ObjectBody, or null.
805
- * @example
806
- * ```typescript
807
- * const body = await api.get(env, "assets/logo.png");
808
- * ```
809
- */
810
- get: (env, key) => provider(env).get(key),
811
- /**
812
- * Write an object to the bucket.
813
- *
814
- * @param env - Per-request Cloudflare bindings.
815
- * @param key - Object key.
816
- * @param value - Object contents (ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null).
817
- * @returns {Promise<R2Object>} The R2Object metadata for the written object.
818
- * @example
819
- * ```typescript
820
- * const obj = await api.put(env, "assets/logo.png", buffer);
821
- * ```
822
- */
823
- put: (env, key, value) => provider(env).put(key, value),
824
- /**
825
- * Remove an object (or array of keys) from the bucket. No-op when absent.
826
- *
827
- * @param env - Per-request Cloudflare bindings.
828
- * @param key - Object key or array of keys.
829
- * @returns {Promise<void>} Resolves once removed.
830
- * @example
831
- * ```typescript
832
- * await api.delete(env, "assets/old.png");
833
- * ```
834
- */
835
- delete: (env, key) => provider(env).delete(key),
836
- /**
837
- * List objects, optionally filtered by R2ListOptions.
838
- *
839
- * @param env - Per-request Cloudflare bindings.
840
- * @param opts - Optional R2ListOptions (prefix, limit, cursor, delimiter).
841
- * @returns {Promise<R2Objects>} The R2Objects list result.
842
- * @example
843
- * ```typescript
844
- * const { objects } = await api.list(env, { prefix: "images/" });
845
- * ```
846
- */
847
- list: (env, opts) => provider(env).list(opts),
848
- /**
849
- * Return this plugin's deploy metadata. Build-time only — does not touch
850
- * `env` or R2. The deploy plugin reads this via `ctx.require(storagePlugin).deployManifest()`.
851
- *
852
- * @returns {StorageManifest} Deploy manifest entry `{ kind: "r2", bucket, upload }`.
853
- * @example
854
- * ```typescript
855
- * const manifest = api.deployManifest();
856
- * // { kind: "r2", bucket: "ASSETS", upload: "./public" }
857
- * ```
858
- */
859
- deployManifest: () => ({
860
- kind: "r2",
861
- bucket: ctx.config.bucket,
862
- upload: ctx.config.upload
863
- })
864
- };
865
- };
866
- /**
867
- * Complex tier — Cloudflare R2 object storage behind a provider adapter seam.
868
- *
869
- * Exposes `get`, `put`, `delete`, `list` (all env-first) and `deployManifest()`
870
- * (build-time). Depends on `bindingsPlugin` to resolve the `R2Bucket` binding
871
- * per request. No state, no events, no lifecycle hooks.
872
- *
873
- * @see README.md
874
- */
875
- const storagePlugin = createPlugin("storage", {
876
- depends: [bindingsPlugin],
877
- config: {
878
- upload: "",
879
- bucket: "ASSETS"
880
- },
881
- api: createStorageApi
882
- });
883
- //#endregion
884
- export { defineDurableObject as a, coreConfig as c, stagePlugin as d, durableObjectsPlugin as i, createCore as l, queuesPlugin as n, d1Plugin as o, kvPlugin as r, bindingsPlugin as s, storagePlugin as t, createPlugin as u };