@moku-labs/worker 0.7.3 → 0.8.1

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.
@@ -739,7 +739,8 @@ const createQueuesApi = (ctx) => {
739
739
  kind: "queue",
740
740
  name: instance.name,
741
741
  binding: instance.binding,
742
- ...instance.onMessage ? { consumer: true } : {}
742
+ ...instance.onMessage ? { consumer: true } : {},
743
+ ...instance.maxBatchTimeout === void 0 ? {} : { maxBatchTimeout: instance.maxBatchTimeout }
743
744
  }))
744
745
  };
745
746
  };
@@ -1700,7 +1701,8 @@ const buildSite = async (ctx, webBuild) => {
1700
1701
  * @file deploy plugin — debounced filesystem watcher for dev.
1701
1702
  *
1702
1703
  * Watches the top-level directories implied by the config globs (recursive) and fires a debounced
1703
- * change callback with the last changed path. Uses node:fs.watch no extra dependency.
1704
+ * change callback with the SET of paths changed in the window (so a burst of edits coalesces into
1705
+ * one rebuild that knows every changed file). Uses node:fs.watch — no extra dependency.
1704
1706
  * Node-only; never imported by the runtime Worker bundle.
1705
1707
  */
1706
1708
  /**
@@ -1723,27 +1725,29 @@ const watchDirectories = (globs) => {
1723
1725
  return [...directories];
1724
1726
  };
1725
1727
  /**
1726
- * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with
1727
- * the last changed path. Missing directories are skipped silently.
1728
+ * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with the
1729
+ * distinct set of paths changed within the window. Missing directories are skipped silently.
1728
1730
  *
1729
1731
  * @param globs - Watch globs.
1730
1732
  * @param debounceMs - Coalesce rapid changes into one callback within this window.
1731
- * @param onChange - Called with the last changed path after the debounce settles.
1733
+ * @param onChange - Called with the changed paths (snapshot of the window) after the debounce settles.
1732
1734
  * @returns A handle whose close() stops all watchers and cancels any pending callback.
1733
1735
  * @example
1734
1736
  * ```ts
1735
- * const handle = watchPaths(["src/**\/*.ts"], 120, p => rebuild(p));
1737
+ * const handle = watchPaths(["src/**\/*.ts"], 120, paths => rebuild(paths));
1736
1738
  * handle.close();
1737
1739
  * ```
1738
1740
  */
1739
1741
  const watchPaths = (globs, debounceMs, onChange) => {
1740
1742
  let timer;
1741
- let lastPath = "";
1743
+ const changed = /* @__PURE__ */ new Set();
1742
1744
  const fire = (changedPath) => {
1743
- lastPath = changedPath;
1745
+ changed.add(changedPath);
1744
1746
  if (timer !== void 0) clearTimeout(timer);
1745
1747
  timer = setTimeout(() => {
1746
- onChange(lastPath);
1748
+ const batch = [...changed];
1749
+ changed.clear();
1750
+ onChange(batch);
1747
1751
  }, debounceMs);
1748
1752
  };
1749
1753
  const watchers = [];
@@ -1873,27 +1877,50 @@ const realDevDeps = () => ({
1873
1877
  */
1874
1878
  const d1MigrationBindings = (ctx) => ctx.has("d1") ? ctx.require(d1Plugin).deployManifest().filter((manifest) => manifest.migrations !== void 0).map((manifest) => manifest.binding) : [];
1875
1879
  /**
1876
- * Rebuild the site once and announce the result. A failed build keeps the session alive (it just
1877
- * emits dev:error and serves the last good build).
1880
+ * One-line description of a changed-path batch for the `dev:phase rebuild` detail: the single path,
1881
+ * or the first path plus a `(+N more)` tail. Empty batches (defensive) read as "site".
1882
+ *
1883
+ * @param paths - The changed paths the watcher coalesced for this rebuild.
1884
+ * @returns The detail string for the rebuild phase event.
1885
+ * @example
1886
+ * ```ts
1887
+ * describeChanges(["src/a.ts", "src/b.css"]); // "src/a.ts (+1 more)"
1888
+ * ```
1889
+ */
1890
+ const describeChanges = (paths) => {
1891
+ const [first, ...rest] = paths;
1892
+ if (first === void 0) return "site";
1893
+ return rest.length === 0 ? first : `${first} (+${String(rest.length)} more)`;
1894
+ };
1895
+ /**
1896
+ * Rebuild the site once for a changed-path batch and announce the result. The FAST path is the
1897
+ * incremental `onChange(changedPaths)` hook (e.g. `web.cli.update`) when wired; otherwise it falls
1898
+ * back to a full `webBuild()` rebuild (via deps.build) — the prior behavior. A failed rebuild keeps
1899
+ * the session alive (it just emits dev:error and serves the last good build). Both paths share one
1900
+ * `dev:phase rebuild` → `dev:rebuilt`/`dev:error` envelope so the branded dev TUI is identical.
1878
1901
  *
1879
1902
  * @param ctx - The deploy plugin context.
1880
1903
  * @param deps - The injected dev deps.
1881
- * @param changedPath - The path that triggered the rebuild.
1882
- * @param webBuild - Optional call-time web build hook threaded into the rebuild.
1904
+ * @param changedPaths - The paths that triggered the rebuild (the watcher's debounced set).
1905
+ * @param hooks - The consumer rebuild hooks.
1906
+ * @param hooks.webBuild - Full rebuild (used when `onChange` is absent — the prior behavior).
1907
+ * @param hooks.onChange - Incremental rebuild for the changed set (the fast path when wired).
1883
1908
  * @returns Resolves once the rebuild attempt completes.
1884
1909
  * @example
1885
1910
  * ```ts
1886
- * await rebuild(ctx, deps, "src/app.tsx", () => web.cli.build());
1911
+ * await rebuild(ctx, deps, ["src/app.tsx"], { onChange: c => web.cli.update(c) });
1887
1912
  * ```
1888
1913
  */
1889
- const rebuild = async (ctx, deps, changedPath, webBuild) => {
1914
+ const rebuild = async (ctx, deps, changedPaths, hooks) => {
1890
1915
  ctx.emit("dev:phase", {
1891
1916
  phase: "rebuild",
1892
- detail: changedPath
1917
+ detail: describeChanges(changedPaths)
1893
1918
  });
1894
1919
  const started = deps.now();
1895
1920
  try {
1896
- const { files } = await deps.build(ctx, webBuild);
1921
+ let files;
1922
+ if (hooks.onChange) files = fileCountOf(await hooks.onChange(changedPaths));
1923
+ else files = (await deps.build(ctx, hooks.webBuild)).files;
1897
1924
  ctx.emit("dev:rebuilt", {
1898
1925
  files,
1899
1926
  ms: deps.now() - started
@@ -1909,17 +1936,20 @@ const rebuild = async (ctx, deps, changedPath, webBuild) => {
1909
1936
  * @param ctx - The deploy plugin context (config + emit + require/has).
1910
1937
  * @param opts - Optional options.
1911
1938
  * @param opts.port - Local dev port (default 8787).
1912
- * @param opts.webBuild - Web build hook (re)run on cold build + each change (e.g. `() => web.cli.build()`).
1939
+ * @param opts.webBuild - Cold-build hook (also the per-change rebuild when `onChange` is omitted).
1940
+ * @param opts.onChange - Incremental per-change rebuild hook (e.g. `c => web.cli.update(c)`); when
1941
+ * set, each debounced change rebuilds only the changed paths instead of a full `webBuild()`.
1913
1942
  * @param deps - Injected side effects (real ones from realDevDeps in production).
1914
1943
  * @returns Resolves when the session ends (SIGINT).
1915
1944
  * @example
1916
1945
  * ```ts
1917
- * await runDev(ctx, { port: 8787, webBuild: () => web.cli.build() }, realDevDeps());
1946
+ * await runDev(ctx, { port: 8787, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) }, realDevDeps());
1918
1947
  * ```
1919
1948
  */
1920
1949
  const runDev = async (ctx, opts, deps) => {
1921
1950
  const port = opts?.port ?? 8787;
1922
1951
  const webBuild = opts?.webBuild;
1952
+ const onChange = opts?.onChange;
1923
1953
  ctx.emit("dev:phase", {
1924
1954
  phase: "build",
1925
1955
  detail: "site"
@@ -1951,7 +1981,10 @@ const runDev = async (ctx, opts, deps) => {
1951
1981
  ctx.config.configFile,
1952
1982
  "--live-reload"
1953
1983
  ]);
1954
- const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPath) => rebuild(ctx, deps, changedPath, webBuild));
1984
+ const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPaths) => rebuild(ctx, deps, changedPaths, {
1985
+ webBuild,
1986
+ onChange
1987
+ }));
1955
1988
  await Promise.race([deps.untilSignal(), child.whenExited]);
1956
1989
  ctx.emit("dev:phase", { phase: "stopping" });
1957
1990
  watcher.close();
@@ -2611,13 +2644,14 @@ const buildD1Databases = (resources, ids) => resources.filter((resource) => reso
2611
2644
  * Every queue is a `producer`; a queue flagged `consumer: true` (it declares an `onMessage` handler)
2612
2645
  * is ALSO registered as a `consumer` so wrangler delivers its messages to this Worker's queue()
2613
2646
  * handler — both locally under `wrangler dev` and in production. Without the consumer entry the
2614
- * handler never runs (the bug that silently drops a queue-driven activity feed).
2647
+ * handler never runs (the bug that silently drops a queue-driven activity feed). A consumer that
2648
+ * sets `maxBatchTimeout` carries it through as wrangler's `max_batch_timeout` (lower delivery latency).
2615
2649
  *
2616
2650
  * @param resources - All resource descriptors from the manifest.
2617
2651
  * @returns The queues section (producers, plus consumers when any), or undefined when there are none.
2618
2652
  * @example
2619
2653
  * ```ts
2620
- * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY", consumer: true }]);
2654
+ * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY", consumer: true, maxBatchTimeout: 1 }]);
2621
2655
  * ```
2622
2656
  */
2623
2657
  const buildQueues = (resources) => {
@@ -2627,7 +2661,11 @@ const buildQueues = (resources) => {
2627
2661
  queue: resource.name,
2628
2662
  binding: resource.binding
2629
2663
  }));
2630
- const consumers = queueResources.filter((resource) => resource.consumer === true).map((resource) => ({ queue: resource.name }));
2664
+ const consumers = queueResources.filter((resource) => resource.consumer === true).map((resource) => {
2665
+ const entry = { queue: resource.name };
2666
+ if (resource.maxBatchTimeout !== void 0) entry.max_batch_timeout = resource.maxBatchTimeout;
2667
+ return entry;
2668
+ });
2631
2669
  return consumers.length > 0 ? {
2632
2670
  producers,
2633
2671
  consumers
@@ -3219,11 +3257,13 @@ const createDeployApi = (ctx) => ({
3219
3257
  * @param opts - Optional options.
3220
3258
  * @param opts.port - Local dev port (default 8787).
3221
3259
  * @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
3222
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3260
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3261
+ * per-change rebuild when `onChange` is omitted.
3262
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`).
3223
3263
  * @returns Resolves when the dev session ends.
3224
3264
  * @example
3225
3265
  * ```ts
3226
- * await api.dev({ port: 8787, webBuild: () => web.cli.build() });
3266
+ * await api.dev({ port: 8787, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3227
3267
  * ```
3228
3268
  */
3229
3269
  async dev(opts) {
@@ -3476,11 +3516,14 @@ const createCliApi = (ctx) => ({
3476
3516
  * @param opts - Optional local dev options.
3477
3517
  * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
3478
3518
  * @param opts.stage - Stage for the generated wrangler config; falls back to `--stage` then the app stage.
3479
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3519
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3520
+ * per-change rebuild when `onChange` is omitted.
3521
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`),
3522
+ * so each change rebuilds only the changed paths instead of a full `webBuild()`.
3480
3523
  * @returns Resolves when the dev session ends.
3481
3524
  * @example
3482
3525
  * ```ts
3483
- * await api.dev({ port: 7878, webBuild: () => web.cli.build() });
3526
+ * await api.dev({ port: 7878, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3484
3527
  * ```
3485
3528
  */
3486
3529
  async dev(opts) {
@@ -3494,7 +3537,8 @@ const createCliApi = (ctx) => ({
3494
3537
  await ctx.require(deployPlugin).dev({
3495
3538
  ...opts?.port === void 0 ? {} : { port: opts.port },
3496
3539
  ...stage === void 0 ? {} : { stage },
3497
- ...opts?.webBuild ? { webBuild: opts.webBuild } : {}
3540
+ ...opts?.webBuild ? { webBuild: opts.webBuild } : {},
3541
+ ...opts?.onChange ? { onChange: opts.onChange } : {}
3498
3542
  });
3499
3543
  ui.check(true, "dev session stopped cleanly");
3500
3544
  } catch (error) {
@@ -762,7 +762,8 @@ const createQueuesApi = (ctx) => {
762
762
  kind: "queue",
763
763
  name: instance.name,
764
764
  binding: instance.binding,
765
- ...instance.onMessage ? { consumer: true } : {}
765
+ ...instance.onMessage ? { consumer: true } : {},
766
+ ...instance.maxBatchTimeout === void 0 ? {} : { maxBatchTimeout: instance.maxBatchTimeout }
766
767
  }))
767
768
  };
768
769
  };
@@ -1723,7 +1724,8 @@ const buildSite = async (ctx, webBuild) => {
1723
1724
  * @file deploy plugin — debounced filesystem watcher for dev.
1724
1725
  *
1725
1726
  * Watches the top-level directories implied by the config globs (recursive) and fires a debounced
1726
- * change callback with the last changed path. Uses node:fs.watch no extra dependency.
1727
+ * change callback with the SET of paths changed in the window (so a burst of edits coalesces into
1728
+ * one rebuild that knows every changed file). Uses node:fs.watch — no extra dependency.
1727
1729
  * Node-only; never imported by the runtime Worker bundle.
1728
1730
  */
1729
1731
  /**
@@ -1746,27 +1748,29 @@ const watchDirectories = (globs) => {
1746
1748
  return [...directories];
1747
1749
  };
1748
1750
  /**
1749
- * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with
1750
- * the last changed path. Missing directories are skipped silently.
1751
+ * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with the
1752
+ * distinct set of paths changed within the window. Missing directories are skipped silently.
1751
1753
  *
1752
1754
  * @param globs - Watch globs.
1753
1755
  * @param debounceMs - Coalesce rapid changes into one callback within this window.
1754
- * @param onChange - Called with the last changed path after the debounce settles.
1756
+ * @param onChange - Called with the changed paths (snapshot of the window) after the debounce settles.
1755
1757
  * @returns A handle whose close() stops all watchers and cancels any pending callback.
1756
1758
  * @example
1757
1759
  * ```ts
1758
- * const handle = watchPaths(["src/**\/*.ts"], 120, p => rebuild(p));
1760
+ * const handle = watchPaths(["src/**\/*.ts"], 120, paths => rebuild(paths));
1759
1761
  * handle.close();
1760
1762
  * ```
1761
1763
  */
1762
1764
  const watchPaths = (globs, debounceMs, onChange) => {
1763
1765
  let timer;
1764
- let lastPath = "";
1766
+ const changed = /* @__PURE__ */ new Set();
1765
1767
  const fire = (changedPath) => {
1766
- lastPath = changedPath;
1768
+ changed.add(changedPath);
1767
1769
  if (timer !== void 0) clearTimeout(timer);
1768
1770
  timer = setTimeout(() => {
1769
- onChange(lastPath);
1771
+ const batch = [...changed];
1772
+ changed.clear();
1773
+ onChange(batch);
1770
1774
  }, debounceMs);
1771
1775
  };
1772
1776
  const watchers = [];
@@ -1896,27 +1900,50 @@ const realDevDeps = () => ({
1896
1900
  */
1897
1901
  const d1MigrationBindings = (ctx) => ctx.has("d1") ? ctx.require(d1Plugin).deployManifest().filter((manifest) => manifest.migrations !== void 0).map((manifest) => manifest.binding) : [];
1898
1902
  /**
1899
- * Rebuild the site once and announce the result. A failed build keeps the session alive (it just
1900
- * emits dev:error and serves the last good build).
1903
+ * One-line description of a changed-path batch for the `dev:phase rebuild` detail: the single path,
1904
+ * or the first path plus a `(+N more)` tail. Empty batches (defensive) read as "site".
1905
+ *
1906
+ * @param paths - The changed paths the watcher coalesced for this rebuild.
1907
+ * @returns The detail string for the rebuild phase event.
1908
+ * @example
1909
+ * ```ts
1910
+ * describeChanges(["src/a.ts", "src/b.css"]); // "src/a.ts (+1 more)"
1911
+ * ```
1912
+ */
1913
+ const describeChanges = (paths) => {
1914
+ const [first, ...rest] = paths;
1915
+ if (first === void 0) return "site";
1916
+ return rest.length === 0 ? first : `${first} (+${String(rest.length)} more)`;
1917
+ };
1918
+ /**
1919
+ * Rebuild the site once for a changed-path batch and announce the result. The FAST path is the
1920
+ * incremental `onChange(changedPaths)` hook (e.g. `web.cli.update`) when wired; otherwise it falls
1921
+ * back to a full `webBuild()` rebuild (via deps.build) — the prior behavior. A failed rebuild keeps
1922
+ * the session alive (it just emits dev:error and serves the last good build). Both paths share one
1923
+ * `dev:phase rebuild` → `dev:rebuilt`/`dev:error` envelope so the branded dev TUI is identical.
1901
1924
  *
1902
1925
  * @param ctx - The deploy plugin context.
1903
1926
  * @param deps - The injected dev deps.
1904
- * @param changedPath - The path that triggered the rebuild.
1905
- * @param webBuild - Optional call-time web build hook threaded into the rebuild.
1927
+ * @param changedPaths - The paths that triggered the rebuild (the watcher's debounced set).
1928
+ * @param hooks - The consumer rebuild hooks.
1929
+ * @param hooks.webBuild - Full rebuild (used when `onChange` is absent — the prior behavior).
1930
+ * @param hooks.onChange - Incremental rebuild for the changed set (the fast path when wired).
1906
1931
  * @returns Resolves once the rebuild attempt completes.
1907
1932
  * @example
1908
1933
  * ```ts
1909
- * await rebuild(ctx, deps, "src/app.tsx", () => web.cli.build());
1934
+ * await rebuild(ctx, deps, ["src/app.tsx"], { onChange: c => web.cli.update(c) });
1910
1935
  * ```
1911
1936
  */
1912
- const rebuild = async (ctx, deps, changedPath, webBuild) => {
1937
+ const rebuild = async (ctx, deps, changedPaths, hooks) => {
1913
1938
  ctx.emit("dev:phase", {
1914
1939
  phase: "rebuild",
1915
- detail: changedPath
1940
+ detail: describeChanges(changedPaths)
1916
1941
  });
1917
1942
  const started = deps.now();
1918
1943
  try {
1919
- const { files } = await deps.build(ctx, webBuild);
1944
+ let files;
1945
+ if (hooks.onChange) files = fileCountOf(await hooks.onChange(changedPaths));
1946
+ else files = (await deps.build(ctx, hooks.webBuild)).files;
1920
1947
  ctx.emit("dev:rebuilt", {
1921
1948
  files,
1922
1949
  ms: deps.now() - started
@@ -1932,17 +1959,20 @@ const rebuild = async (ctx, deps, changedPath, webBuild) => {
1932
1959
  * @param ctx - The deploy plugin context (config + emit + require/has).
1933
1960
  * @param opts - Optional options.
1934
1961
  * @param opts.port - Local dev port (default 8787).
1935
- * @param opts.webBuild - Web build hook (re)run on cold build + each change (e.g. `() => web.cli.build()`).
1962
+ * @param opts.webBuild - Cold-build hook (also the per-change rebuild when `onChange` is omitted).
1963
+ * @param opts.onChange - Incremental per-change rebuild hook (e.g. `c => web.cli.update(c)`); when
1964
+ * set, each debounced change rebuilds only the changed paths instead of a full `webBuild()`.
1936
1965
  * @param deps - Injected side effects (real ones from realDevDeps in production).
1937
1966
  * @returns Resolves when the session ends (SIGINT).
1938
1967
  * @example
1939
1968
  * ```ts
1940
- * await runDev(ctx, { port: 8787, webBuild: () => web.cli.build() }, realDevDeps());
1969
+ * await runDev(ctx, { port: 8787, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) }, realDevDeps());
1941
1970
  * ```
1942
1971
  */
1943
1972
  const runDev = async (ctx, opts, deps) => {
1944
1973
  const port = opts?.port ?? 8787;
1945
1974
  const webBuild = opts?.webBuild;
1975
+ const onChange = opts?.onChange;
1946
1976
  ctx.emit("dev:phase", {
1947
1977
  phase: "build",
1948
1978
  detail: "site"
@@ -1974,7 +2004,10 @@ const runDev = async (ctx, opts, deps) => {
1974
2004
  ctx.config.configFile,
1975
2005
  "--live-reload"
1976
2006
  ]);
1977
- const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPath) => rebuild(ctx, deps, changedPath, webBuild));
2007
+ const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPaths) => rebuild(ctx, deps, changedPaths, {
2008
+ webBuild,
2009
+ onChange
2010
+ }));
1978
2011
  await Promise.race([deps.untilSignal(), child.whenExited]);
1979
2012
  ctx.emit("dev:phase", { phase: "stopping" });
1980
2013
  watcher.close();
@@ -2634,13 +2667,14 @@ const buildD1Databases = (resources, ids) => resources.filter((resource) => reso
2634
2667
  * Every queue is a `producer`; a queue flagged `consumer: true` (it declares an `onMessage` handler)
2635
2668
  * is ALSO registered as a `consumer` so wrangler delivers its messages to this Worker's queue()
2636
2669
  * handler — both locally under `wrangler dev` and in production. Without the consumer entry the
2637
- * handler never runs (the bug that silently drops a queue-driven activity feed).
2670
+ * handler never runs (the bug that silently drops a queue-driven activity feed). A consumer that
2671
+ * sets `maxBatchTimeout` carries it through as wrangler's `max_batch_timeout` (lower delivery latency).
2638
2672
  *
2639
2673
  * @param resources - All resource descriptors from the manifest.
2640
2674
  * @returns The queues section (producers, plus consumers when any), or undefined when there are none.
2641
2675
  * @example
2642
2676
  * ```ts
2643
- * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY", consumer: true }]);
2677
+ * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY", consumer: true, maxBatchTimeout: 1 }]);
2644
2678
  * ```
2645
2679
  */
2646
2680
  const buildQueues = (resources) => {
@@ -2650,7 +2684,11 @@ const buildQueues = (resources) => {
2650
2684
  queue: resource.name,
2651
2685
  binding: resource.binding
2652
2686
  }));
2653
- const consumers = queueResources.filter((resource) => resource.consumer === true).map((resource) => ({ queue: resource.name }));
2687
+ const consumers = queueResources.filter((resource) => resource.consumer === true).map((resource) => {
2688
+ const entry = { queue: resource.name };
2689
+ if (resource.maxBatchTimeout !== void 0) entry.max_batch_timeout = resource.maxBatchTimeout;
2690
+ return entry;
2691
+ });
2654
2692
  return consumers.length > 0 ? {
2655
2693
  producers,
2656
2694
  consumers
@@ -3242,11 +3280,13 @@ const createDeployApi = (ctx) => ({
3242
3280
  * @param opts - Optional options.
3243
3281
  * @param opts.port - Local dev port (default 8787).
3244
3282
  * @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
3245
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3283
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3284
+ * per-change rebuild when `onChange` is omitted.
3285
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`).
3246
3286
  * @returns Resolves when the dev session ends.
3247
3287
  * @example
3248
3288
  * ```ts
3249
- * await api.dev({ port: 8787, webBuild: () => web.cli.build() });
3289
+ * await api.dev({ port: 8787, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3250
3290
  * ```
3251
3291
  */
3252
3292
  async dev(opts) {
@@ -3499,11 +3539,14 @@ const createCliApi = (ctx) => ({
3499
3539
  * @param opts - Optional local dev options.
3500
3540
  * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
3501
3541
  * @param opts.stage - Stage for the generated wrangler config; falls back to `--stage` then the app stage.
3502
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3542
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3543
+ * per-change rebuild when `onChange` is omitted.
3544
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`),
3545
+ * so each change rebuilds only the changed paths instead of a full `webBuild()`.
3503
3546
  * @returns Resolves when the dev session ends.
3504
3547
  * @example
3505
3548
  * ```ts
3506
- * await api.dev({ port: 7878, webBuild: () => web.cli.build() });
3549
+ * await api.dev({ port: 7878, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3507
3550
  * ```
3508
3551
  */
3509
3552
  async dev(opts) {
@@ -3517,7 +3560,8 @@ const createCliApi = (ctx) => ({
3517
3560
  await ctx.require(deployPlugin).dev({
3518
3561
  ...opts?.port === void 0 ? {} : { port: opts.port },
3519
3562
  ...stage === void 0 ? {} : { stage },
3520
- ...opts?.webBuild ? { webBuild: opts.webBuild } : {}
3563
+ ...opts?.webBuild ? { webBuild: opts.webBuild } : {},
3564
+ ...opts?.onChange ? { onChange: opts.onChange } : {}
3521
3565
  });
3522
3566
  ui.check(true, "dev session stopped cleanly");
3523
3567
  } catch (error) {
package/dist/cli.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cli = require("./cli-DQcpvh2s.cjs");
2
+ const require_cli = require("./cli-l-AOWzhR.cjs");
3
3
  exports.cliPlugin = require_cli.cliPlugin;
4
4
  exports.deployPlugin = require_cli.deployPlugin;
package/dist/cli.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-DCweBI9s.cjs";
1
+ import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-BDkgen4r.cjs";
2
2
  export { type ExternalManifest, type ResourceManifest, cliPlugin, deployPlugin };
package/dist/cli.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-DCweBI9s.mjs";
1
+ import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-BDkgen4r.mjs";
2
2
  export { type ExternalManifest, type ResourceManifest, cliPlugin, deployPlugin };
package/dist/cli.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as deployPlugin, t as cliPlugin } from "./cli-MICK1cvv.mjs";
1
+ import { n as deployPlugin, t as cliPlugin } from "./cli-DNW8_355.mjs";
2
2
  export { cliPlugin, deployPlugin };
@@ -102,6 +102,23 @@ type WorkerPluginCtx<Config, State, Events extends Record<string, unknown> = Rec
102
102
  * ```
103
103
  */
104
104
  type WebBuild = () => Promise<unknown>;
105
+ /**
106
+ * A per-change INCREMENTAL rebuild hook wired in from the consumer's dev script — e.g.
107
+ * `(changes) => webApp.cli.update(changes)`. The fast counterpart to {@link WebBuild}: `dev`
108
+ * calls {@link WebBuild} ONCE for the cold build, then (when this hook is wired) calls `onChange`
109
+ * with the set of paths changed in the debounce window so the web build can rebuild only what
110
+ * changed instead of doing a full `webBuild()` every keystroke. Omit it and `dev` keeps doing a
111
+ * full `webBuild()` per change (the prior behavior). Like {@link WebBuild} it may resolve ANYTHING
112
+ * (the web build's own summary); the value is read opportunistically for a `files` count.
113
+ *
114
+ * @param changes - The paths changed since the last rebuild (the watcher's debounced set).
115
+ * @returns Resolves when the incremental rebuild completes.
116
+ * @example
117
+ * ```ts
118
+ * await server.cli.dev({ webBuild: () => web.cli.build(), onChange: changes => web.cli.update(changes) });
119
+ * ```
120
+ */
121
+ type OnChange = (changes: readonly string[]) => Promise<unknown>;
105
122
  /** deploy plugin configuration. Flat; complete defaults so omission never yields undefined. */
106
123
  type Config$1 = {
107
124
  /**
@@ -179,6 +196,7 @@ type ResourceManifest = {
179
196
  name: string;
180
197
  binding: string;
181
198
  consumer?: boolean;
199
+ maxBatchTimeout?: number;
182
200
  } | {
183
201
  kind: "do";
184
202
  binding: string;
@@ -267,22 +285,26 @@ type Api = {
267
285
  * when omitted. A failure renders a branded `✗` line and sets a non-zero exit code rather than
268
286
  * throwing a raw stack trace.
269
287
  *
270
- * @param opts - Optional port, stage, and web build hook.
288
+ * @param opts - Optional port, stage, cold-build hook, and incremental change hook.
271
289
  * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
272
290
  * @param opts.stage - Stage for the generated wrangler config's resource names. Falls back to the
273
291
  * `--stage` CLI flag, then the app's configured stage. Pass it explicitly from a script for a
274
292
  * self-documenting `dev({ stage })` instead of relying on the hidden flag.
275
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
293
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
294
+ * per-change rebuild when `onChange` is omitted.
295
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`),
296
+ * so each change rebuilds only the changed paths instead of a full `webBuild()` every keystroke.
276
297
  * @returns Resolves when the dev session ends.
277
298
  * @example
278
299
  * ```ts
279
- * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build() });
300
+ * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
280
301
  * ```
281
302
  */
282
303
  dev(opts?: {
283
304
  port?: number;
284
305
  stage?: string;
285
306
  webBuild?: WebBuild;
307
+ onChange?: OnChange;
286
308
  }): Promise<void>;
287
309
  /**
288
310
  * One-command Cloudflare deploy (delegates to deploy.run). Guided/interactive by default; pass
@@ -414,6 +436,7 @@ declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", C
414
436
  port?: number;
415
437
  stage?: string;
416
438
  webBuild?: WebBuild;
439
+ onChange?: OnChange;
417
440
  }): Promise<void>;
418
441
  seed(sqlFile: string, opts?: {
419
442
  stage?: string;
@@ -102,6 +102,23 @@ type WorkerPluginCtx<Config, State, Events extends Record<string, unknown> = Rec
102
102
  * ```
103
103
  */
104
104
  type WebBuild = () => Promise<unknown>;
105
+ /**
106
+ * A per-change INCREMENTAL rebuild hook wired in from the consumer's dev script — e.g.
107
+ * `(changes) => webApp.cli.update(changes)`. The fast counterpart to {@link WebBuild}: `dev`
108
+ * calls {@link WebBuild} ONCE for the cold build, then (when this hook is wired) calls `onChange`
109
+ * with the set of paths changed in the debounce window so the web build can rebuild only what
110
+ * changed instead of doing a full `webBuild()` every keystroke. Omit it and `dev` keeps doing a
111
+ * full `webBuild()` per change (the prior behavior). Like {@link WebBuild} it may resolve ANYTHING
112
+ * (the web build's own summary); the value is read opportunistically for a `files` count.
113
+ *
114
+ * @param changes - The paths changed since the last rebuild (the watcher's debounced set).
115
+ * @returns Resolves when the incremental rebuild completes.
116
+ * @example
117
+ * ```ts
118
+ * await server.cli.dev({ webBuild: () => web.cli.build(), onChange: changes => web.cli.update(changes) });
119
+ * ```
120
+ */
121
+ type OnChange = (changes: readonly string[]) => Promise<unknown>;
105
122
  /** deploy plugin configuration. Flat; complete defaults so omission never yields undefined. */
106
123
  type Config$1 = {
107
124
  /**
@@ -179,6 +196,7 @@ type ResourceManifest = {
179
196
  name: string;
180
197
  binding: string;
181
198
  consumer?: boolean;
199
+ maxBatchTimeout?: number;
182
200
  } | {
183
201
  kind: "do";
184
202
  binding: string;
@@ -267,22 +285,26 @@ type Api = {
267
285
  * when omitted. A failure renders a branded `✗` line and sets a non-zero exit code rather than
268
286
  * throwing a raw stack trace.
269
287
  *
270
- * @param opts - Optional port, stage, and web build hook.
288
+ * @param opts - Optional port, stage, cold-build hook, and incremental change hook.
271
289
  * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
272
290
  * @param opts.stage - Stage for the generated wrangler config's resource names. Falls back to the
273
291
  * `--stage` CLI flag, then the app's configured stage. Pass it explicitly from a script for a
274
292
  * self-documenting `dev({ stage })` instead of relying on the hidden flag.
275
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
293
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
294
+ * per-change rebuild when `onChange` is omitted.
295
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`),
296
+ * so each change rebuilds only the changed paths instead of a full `webBuild()` every keystroke.
276
297
  * @returns Resolves when the dev session ends.
277
298
  * @example
278
299
  * ```ts
279
- * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build() });
300
+ * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
280
301
  * ```
281
302
  */
282
303
  dev(opts?: {
283
304
  port?: number;
284
305
  stage?: string;
285
306
  webBuild?: WebBuild;
307
+ onChange?: OnChange;
286
308
  }): Promise<void>;
287
309
  /**
288
310
  * One-command Cloudflare deploy (delegates to deploy.run). Guided/interactive by default; pass
@@ -414,6 +436,7 @@ declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", C
414
436
  port?: number;
415
437
  stage?: string;
416
438
  webBuild?: WebBuild;
439
+ onChange?: OnChange;
417
440
  }): Promise<void>;
418
441
  seed(sqlFile: string, opts?: {
419
442
  stage?: string;
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cli = require("./cli-DQcpvh2s.cjs");
2
+ const require_cli = require("./cli-l-AOWzhR.cjs");
3
3
  let _moku_labs_common = require("@moku-labs/common");
4
4
  //#region src/env-provider.ts
5
5
  /**
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-DCweBI9s.cjs";
1
+ import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-BDkgen4r.cjs";
2
2
  import { envPlugin, logPlugin } from "@moku-labs/common";
3
3
  import { PluginCtx, PluginCtx as PluginCtx$1, PluginInstance } from "@moku-labs/core";
4
4
 
@@ -774,7 +774,14 @@ declare namespace types_d_exports$3 {
774
774
  type QueueInstance = {
775
775
  /** Base Cloudflare queue name (stage-suffixed at deploy, e.g. `tracker-activity-dev`). */name: string; /** Producer env binding the Queue resolves off the per-request `env` (e.g. `env.ACTIVITY`). */
776
776
  binding: string; /** Per-instance consumer handler — awaited once per message in `consume()`. Optional → no-op. */
777
- onMessage?: (message: Message, env: WorkerEnv) => Promise<void>; /** Marks this instance the default when more than one is configured. */
777
+ onMessage?: (message: Message, env: WorkerEnv) => Promise<void>;
778
+ /**
779
+ * Max seconds the consumer waits to fill a batch before delivering it (Cloudflare's
780
+ * `max_batch_timeout`, 0–60). Lower means lower delivery latency and smaller batches; omit to use
781
+ * Cloudflare's default (~5s). Written to the generated wrangler `consumers` entry, so it only has
782
+ * an effect on an instance that also declares an `onMessage` handler.
783
+ */
784
+ maxBatchTimeout?: number; /** Marks this instance the default when more than one is configured. */
778
785
  default?: boolean;
779
786
  };
780
787
  /**
@@ -861,6 +868,7 @@ type Api = QueueProducerApi & {
861
868
  name: string;
862
869
  binding: string;
863
870
  consumer?: boolean;
871
+ maxBatchTimeout?: number;
864
872
  }>;
865
873
  };
866
874
  /**
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-DCweBI9s.mjs";
1
+ import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-BDkgen4r.mjs";
2
2
  import { envPlugin, logPlugin } from "@moku-labs/common";
3
3
  import { PluginCtx, PluginCtx as PluginCtx$1, PluginInstance } from "@moku-labs/core";
4
4
 
@@ -774,7 +774,14 @@ declare namespace types_d_exports$3 {
774
774
  type QueueInstance = {
775
775
  /** Base Cloudflare queue name (stage-suffixed at deploy, e.g. `tracker-activity-dev`). */name: string; /** Producer env binding the Queue resolves off the per-request `env` (e.g. `env.ACTIVITY`). */
776
776
  binding: string; /** Per-instance consumer handler — awaited once per message in `consume()`. Optional → no-op. */
777
- onMessage?: (message: Message, env: WorkerEnv) => Promise<void>; /** Marks this instance the default when more than one is configured. */
777
+ onMessage?: (message: Message, env: WorkerEnv) => Promise<void>;
778
+ /**
779
+ * Max seconds the consumer waits to fill a batch before delivering it (Cloudflare's
780
+ * `max_batch_timeout`, 0–60). Lower means lower delivery latency and smaller batches; omit to use
781
+ * Cloudflare's default (~5s). Written to the generated wrangler `consumers` entry, so it only has
782
+ * an effect on an instance that also declares an `onMessage` handler.
783
+ */
784
+ maxBatchTimeout?: number; /** Marks this instance the default when more than one is configured. */
778
785
  default?: boolean;
779
786
  };
780
787
  /**
@@ -861,6 +868,7 @@ type Api = QueueProducerApi & {
861
868
  name: string;
862
869
  binding: string;
863
870
  consumer?: boolean;
871
+ maxBatchTimeout?: number;
864
872
  }>;
865
873
  };
866
874
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as kvPlugin, c as d1Plugin, d as createCore, f as createPlugin$1, i as queuesPlugin, l as bindingsPlugin, n as deployPlugin, o as durableObjectsPlugin, p as stagePlugin, r as storagePlugin, s as defineDurableObject, t as cliPlugin, u as coreConfig } from "./cli-MICK1cvv.mjs";
1
+ import { a as kvPlugin, c as d1Plugin, d as createCore, f as createPlugin$1, i as queuesPlugin, l as bindingsPlugin, n as deployPlugin, o as durableObjectsPlugin, p as stagePlugin, r as storagePlugin, s as defineDurableObject, t as cliPlugin, u as coreConfig } from "./cli-DNW8_355.mjs";
2
2
  import { envPlugin, logPlugin } from "@moku-labs/common";
3
3
  //#region src/env-provider.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/worker",
3
- "version": "0.7.3",
3
+ "version": "0.8.1",
4
4
  "description": "Cloudflare Worker framework for Moku — Durable Objects, Queues, R2, D1, and KV plugins that compose with Moku Web.",
5
5
  "repository": {
6
6
  "type": "git",