@elurjs/kit 2.4.10 → 2.5.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.
Files changed (188) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +248 -15
  3. package/dist/lib/action/index.cjs +1 -1
  4. package/dist/lib/action/index.js +1 -1
  5. package/dist/lib/{action-DZHihU3Z.cjs → action-BiE4tEty.cjs} +2 -2
  6. package/dist/lib/{action-DZHihU3Z.cjs.map → action-BiE4tEty.cjs.map} +1 -1
  7. package/dist/lib/{action-BB6N5yD-.js → action-CXVk8rWt.js} +3 -3
  8. package/dist/lib/{action-BB6N5yD-.js.map → action-CXVk8rWt.js.map} +1 -1
  9. package/dist/lib/adapter-DNsjXvq5.js +160 -0
  10. package/dist/lib/adapter-DNsjXvq5.js.map +1 -0
  11. package/dist/lib/adapter-Dz2hzR36.cjs +3 -0
  12. package/dist/lib/adapter-Dz2hzR36.cjs.map +1 -0
  13. package/dist/lib/adapters/bun.cjs +2 -2
  14. package/dist/lib/adapters/bun.cjs.map +1 -1
  15. package/dist/lib/adapters/bun.js +17 -9
  16. package/dist/lib/adapters/bun.js.map +1 -1
  17. package/dist/lib/adapters/index.d.cts +34 -0
  18. package/dist/lib/adapters/index.d.ts +34 -0
  19. package/dist/lib/adapters/netlify.cjs +1 -1
  20. package/dist/lib/adapters/netlify.js +7 -7
  21. package/dist/lib/adapters/node.cjs +5 -5
  22. package/dist/lib/adapters/node.cjs.map +1 -1
  23. package/dist/lib/adapters/node.js +20 -12
  24. package/dist/lib/adapters/node.js.map +1 -1
  25. package/dist/lib/adapters/vercel.cjs +1 -1
  26. package/dist/lib/adapters/vercel.js +8 -8
  27. package/dist/lib/build/build.d.cts +40 -0
  28. package/dist/lib/build/build.d.ts +40 -0
  29. package/dist/lib/build/document-shell.d.cts +47 -1
  30. package/dist/lib/build/document-shell.d.ts +47 -1
  31. package/dist/lib/build/vite-build.d.cts +25 -2
  32. package/dist/lib/build/vite-build.d.ts +25 -2
  33. package/dist/lib/cache/adapter.d.cts +41 -0
  34. package/dist/lib/cache/adapter.d.ts +41 -0
  35. package/dist/lib/cache/index.cjs +2 -0
  36. package/dist/lib/cache/index.cjs.map +1 -0
  37. package/dist/lib/cache/index.d.cts +5 -0
  38. package/dist/lib/cache/index.d.ts +5 -0
  39. package/dist/lib/cache/index.js +113 -0
  40. package/dist/lib/cache/index.js.map +1 -0
  41. package/dist/lib/cache/invalidation.d.cts +36 -0
  42. package/dist/lib/cache/invalidation.d.ts +36 -0
  43. package/dist/lib/cache/redis-adapter.d.cts +49 -0
  44. package/dist/lib/cache/redis-adapter.d.ts +49 -0
  45. package/dist/lib/cache-DGhTYyqh.js +50 -0
  46. package/dist/lib/cache-DGhTYyqh.js.map +1 -0
  47. package/dist/lib/cache-X1n4Ossu.cjs +2 -0
  48. package/dist/lib/cache-X1n4Ossu.cjs.map +1 -0
  49. package/dist/lib/cli/output.d.cts +58 -0
  50. package/dist/lib/cli/output.d.ts +58 -0
  51. package/dist/lib/cli/ports.d.cts +20 -0
  52. package/dist/lib/cli/ports.d.ts +20 -0
  53. package/dist/lib/cli.cjs +2559 -1488
  54. package/dist/lib/cli.cjs.map +1 -1
  55. package/dist/lib/cli.d.cts +21 -0
  56. package/dist/lib/cli.d.ts +21 -0
  57. package/dist/lib/cli.js +2558 -1488
  58. package/dist/lib/cli.js.map +1 -1
  59. package/dist/lib/config/index.cjs +1 -1
  60. package/dist/lib/config/index.cjs.map +1 -1
  61. package/dist/lib/config/index.d.cts +79 -0
  62. package/dist/lib/config/index.d.ts +79 -0
  63. package/dist/lib/config/index.js +21 -14
  64. package/dist/lib/config/index.js.map +1 -1
  65. package/dist/lib/{errors-ePYxDlhU.js → errors-j4cg9VN3.js} +24 -30
  66. package/dist/lib/{errors-ePYxDlhU.js.map → errors-j4cg9VN3.js.map} +1 -1
  67. package/dist/lib/errors-kSl9yLkg.cjs +2 -0
  68. package/dist/lib/{errors-0X7pCFNU.cjs.map → errors-kSl9yLkg.cjs.map} +1 -1
  69. package/dist/lib/hydrate-BSLuI0Nh.cjs +2 -0
  70. package/dist/lib/hydrate-BSLuI0Nh.cjs.map +1 -0
  71. package/dist/lib/hydrate-BaS7-d8C.js +133 -0
  72. package/dist/lib/hydrate-BaS7-d8C.js.map +1 -0
  73. package/dist/lib/idiomorph.esm-BOOTOg1r.cjs +2 -0
  74. package/dist/lib/idiomorph.esm-BOOTOg1r.cjs.map +1 -0
  75. package/dist/lib/idiomorph.esm-CXuY-EHB.js +371 -0
  76. package/dist/lib/idiomorph.esm-CXuY-EHB.js.map +1 -0
  77. package/dist/lib/index.cjs +1 -1
  78. package/dist/lib/index.cjs.map +1 -1
  79. package/dist/lib/index.d.cts +8 -6
  80. package/dist/lib/index.d.ts +8 -6
  81. package/dist/lib/index.js +1 -1
  82. package/dist/lib/index.js.map +1 -1
  83. package/dist/lib/interpolation-plugin-C0dTd69H.cjs +58 -0
  84. package/dist/lib/interpolation-plugin-C0dTd69H.cjs.map +1 -0
  85. package/dist/lib/{interpolation-plugin-Wgb1j4pT.js → interpolation-plugin-K2qE69tF.js} +95 -126
  86. package/dist/lib/interpolation-plugin-K2qE69tF.js.map +1 -0
  87. package/dist/lib/invalidation-CFpNQUv6.cjs +2 -0
  88. package/dist/lib/invalidation-CFpNQUv6.cjs.map +1 -0
  89. package/dist/lib/invalidation-DXhdE9wj.js +64 -0
  90. package/dist/lib/invalidation-DXhdE9wj.js.map +1 -0
  91. package/dist/lib/island/generate-entry.d.cts +40 -2
  92. package/dist/lib/island/generate-entry.d.ts +40 -2
  93. package/dist/lib/island/hydrate.d.cts +19 -6
  94. package/dist/lib/island/hydrate.d.ts +19 -6
  95. package/dist/lib/island/index.cjs +1 -1
  96. package/dist/lib/island/index.d.cts +2 -1
  97. package/dist/lib/island/index.d.ts +2 -1
  98. package/dist/lib/island/index.js +1 -1
  99. package/dist/lib/island/island.d.cts +15 -0
  100. package/dist/lib/island/island.d.ts +15 -0
  101. package/dist/lib/island-CTBnsOhI.js +59 -0
  102. package/dist/lib/island-CTBnsOhI.js.map +1 -0
  103. package/dist/lib/island-kfQEKRe_.cjs +2 -0
  104. package/dist/lib/island-kfQEKRe_.cjs.map +1 -0
  105. package/dist/lib/node-http-DVFcYeBS.js +723 -0
  106. package/dist/lib/node-http-DVFcYeBS.js.map +1 -0
  107. package/dist/lib/node-http-Uct7DhmN.cjs +13 -0
  108. package/dist/lib/node-http-Uct7DhmN.cjs.map +1 -0
  109. package/dist/lib/rolldown-runtime-BIInC43l.cjs +1 -0
  110. package/dist/lib/rolldown-runtime-Dy4uBu1J.js +11 -0
  111. package/dist/lib/router/client.cjs +1 -1
  112. package/dist/lib/router/client.cjs.map +1 -1
  113. package/dist/lib/router/client.d.cts +55 -8
  114. package/dist/lib/router/client.d.ts +55 -8
  115. package/dist/lib/router/client.js +298 -97
  116. package/dist/lib/router/client.js.map +1 -1
  117. package/dist/lib/router/redirects.d.cts +34 -0
  118. package/dist/lib/router/redirects.d.ts +34 -0
  119. package/dist/lib/runtime/handler.d.cts +59 -0
  120. package/dist/lib/runtime/handler.d.ts +59 -0
  121. package/dist/lib/runtime/index.cjs +1 -1
  122. package/dist/lib/runtime/index.d.cts +2 -1
  123. package/dist/lib/runtime/index.d.ts +2 -1
  124. package/dist/lib/runtime/index.js +4 -4
  125. package/dist/lib/runtime/logger.d.cts +47 -0
  126. package/dist/lib/runtime/logger.d.ts +47 -0
  127. package/dist/lib/runtime/node-http.d.cts +11 -1
  128. package/dist/lib/runtime/node-http.d.ts +11 -1
  129. package/dist/lib/runtime-ChE4flX3.cjs +2 -0
  130. package/dist/lib/runtime-ChE4flX3.cjs.map +1 -0
  131. package/dist/lib/runtime-OMyz5yE-.js +905 -0
  132. package/dist/lib/runtime-OMyz5yE-.js.map +1 -0
  133. package/dist/lib/seo/index.cjs +1 -10
  134. package/dist/lib/seo/index.d.cts +1 -0
  135. package/dist/lib/seo/index.d.ts +1 -0
  136. package/dist/lib/seo/index.js +2 -41
  137. package/dist/lib/seo/sitemap-from-routes.d.cts +24 -0
  138. package/dist/lib/seo/sitemap-from-routes.d.ts +24 -0
  139. package/dist/lib/{shared-CfqOLsKj.cjs → shared-CP0Dc3cp.cjs} +72 -47
  140. package/dist/lib/shared-CP0Dc3cp.cjs.map +1 -0
  141. package/dist/lib/{shared-DU3iQgr7.js → shared-D8w-neq2.js} +87 -58
  142. package/dist/lib/shared-D8w-neq2.js.map +1 -0
  143. package/dist/lib/sitemap-from-routes-CaBoiGvE.js +77 -0
  144. package/dist/lib/sitemap-from-routes-CaBoiGvE.js.map +1 -0
  145. package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs +13 -0
  146. package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs.map +1 -0
  147. package/dist/lib/ssr/render.d.cts +8 -2
  148. package/dist/lib/ssr/render.d.ts +8 -2
  149. package/dist/lib/ssr/server.d.cts +7 -0
  150. package/dist/lib/ssr/server.d.ts +7 -0
  151. package/dist/lib/ssr/stream-response.d.cts +30 -0
  152. package/dist/lib/ssr/stream-response.d.ts +30 -0
  153. package/dist/lib/ssr/stream.d.cts +16 -1
  154. package/dist/lib/ssr/stream.d.ts +16 -1
  155. package/dist/lib/{stream-DmZjYqHm.cjs → stream-Cg-xYG82.cjs} +3 -3
  156. package/dist/lib/stream-Cg-xYG82.cjs.map +1 -0
  157. package/dist/lib/stream-D-ytogdu.js +106 -0
  158. package/dist/lib/stream-D-ytogdu.js.map +1 -0
  159. package/dist/lib/vite/index.cjs +1 -1
  160. package/dist/lib/vite/index.cjs.map +1 -1
  161. package/dist/lib/vite/index.d.cts +11 -0
  162. package/dist/lib/vite/index.d.ts +11 -0
  163. package/dist/lib/vite/index.js +101 -94
  164. package/dist/lib/vite/index.js.map +1 -1
  165. package/package.json +25 -8
  166. package/dist/lib/errors-0X7pCFNU.cjs +0 -2
  167. package/dist/lib/hydrate-CJbUWdXu.js +0 -126
  168. package/dist/lib/hydrate-CJbUWdXu.js.map +0 -1
  169. package/dist/lib/hydrate-D6MR24mz.cjs +0 -2
  170. package/dist/lib/hydrate-D6MR24mz.cjs.map +0 -1
  171. package/dist/lib/interpolation-plugin-TWDch8nn.cjs +0 -39
  172. package/dist/lib/interpolation-plugin-TWDch8nn.cjs.map +0 -1
  173. package/dist/lib/interpolation-plugin-Wgb1j4pT.js.map +0 -1
  174. package/dist/lib/node-http-DRAUhO0c.js +0 -617
  175. package/dist/lib/node-http-DRAUhO0c.js.map +0 -1
  176. package/dist/lib/node-http-DhxguYyz.cjs +0 -13
  177. package/dist/lib/node-http-DhxguYyz.cjs.map +0 -1
  178. package/dist/lib/runtime-CrH5X-Es.cjs +0 -2
  179. package/dist/lib/runtime-CrH5X-Es.cjs.map +0 -1
  180. package/dist/lib/runtime-Ds2-wH6P.js +0 -520
  181. package/dist/lib/runtime-Ds2-wH6P.js.map +0 -1
  182. package/dist/lib/seo/index.cjs.map +0 -1
  183. package/dist/lib/seo/index.js.map +0 -1
  184. package/dist/lib/shared-CfqOLsKj.cjs.map +0 -1
  185. package/dist/lib/shared-DU3iQgr7.js.map +0 -1
  186. package/dist/lib/stream-CpOG052V.js +0 -104
  187. package/dist/lib/stream-CpOG052V.js.map +0 -1
  188. package/dist/lib/stream-DmZjYqHm.cjs.map +0 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@elurjs/kit.svg)](https://www.npmjs.com/package/@elurjs/kit)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- > Full-stack framework for Elur — file-based routing, SSG, SSR, ISR, streaming, islands, actions, content collections, cache adapters, and SPA-like navigation. Zero extra runtime dependencies on the client: Elur stays at ~15 KB. Optional build-time compiler lowers `html\`\`` templates to imperative DOM code for ~25–44% faster renders.
6
+ > Full-stack framework for Elur — file-based routing, SSG, SSR, ISR, streaming, islands, actions, content collections, cache adapters, and SPA-like navigation. Zero extra runtime dependencies on the client: Elur stays at ~15 KB. Optional build-time compiler lowers ``html` `` templates to imperative DOM code for ~25–44% faster renders.
7
7
 
8
8
  ## What is Elur Kit?
9
9
 
@@ -21,8 +21,8 @@ Elur Kit is a framework built on top of [Elur](https://elur.dev/). It brings con
21
21
  ### Key features
22
22
 
23
23
  - **Routing**: file-based with dynamic segments, optional catch-all `[[...slug]]`, route conflict detection, safe URL decoding, redirects/rewrites/route headers
24
- - **Rendering**: SSG, SSR, ISR with explicit cache policy (public/private/dynamic), streaming with `ReadableStream` (**experimental** — fallback buffered por adapter; ver nota de streaming)
25
- - **Build-time compiler** (optional, recommended): integrates [`@elurjs/core-compiler`](https://www.npmjs.com/package/@elurjs/core-compiler) via [`@elurjs/vite-plugin-elur`](https://www.npmjs.com/package/@elurjs/vite-plugin-elur) to lower `html\`\`` templates to imperative DOM code at build time — eliminates `detectContext`, `buildHTML`, and both `TreeWalker` passes in runtime
24
+ - **Rendering**: SSG, SSR, ISR with explicit cache policy (public/private/dynamic), opt-in streaming SSR with `loading.ts` boundaries (**experimental** — ver [Streaming SSR](#streaming-ssr-experimental))
25
+ - **Build-time compiler** (optional, recommended): integrates [`@elurjs/core-compiler`](https://www.npmjs.com/package/@elurjs/core-compiler) via [`@elurjs/vite-plugin-elur`](https://www.npmjs.com/package/@elurjs/vite-plugin-elur) to lower ``html` `` templates to imperative DOM code at build time — eliminates `detectContext`, `buildHTML`, and both `TreeWalker` passes in runtime
26
26
  - **Partial attribute interpolation**: `class="btn ${size}"` works out of the box via the Vite plugin's state-machine lexer (or the kit's legacy transform as fallback)
27
27
  - **Actions**: typed `defineAction()` with input validation, AbortSignal, idempotency, concurrency modes (latest/queue/parallel)
28
28
  - **Cache**: `CacheAdapter` with filesystem, Redis, and Cloudflare KV storage, SHA-256 keys, atomic writes, single-flight, stale-while-revalidate, tag-based invalidation
@@ -48,7 +48,7 @@ npm install @elurjs/vite-plugin-elur
48
48
 
49
49
  The plugin is an optional peer dependency. When installed, it activates:
50
50
 
51
- - **Build-time compiler** — lowers `html\`\`` to imperative DOM code
51
+ - **Build-time compiler** — lowers ``html` `` to imperative DOM code
52
52
  - **Partial attribute interpolation** — state-machine lexer (replaces the kit's legacy transform)
53
53
  - **HMR with state preservation** — signals, stores, forms, routers survive hot updates
54
54
  - **Scroll/focus preservation** — restored after re-mount
@@ -160,10 +160,10 @@ Options:
160
160
 
161
161
  - **File-based routing** — `src/app/page.ts` maps to URLs with dynamic segments (`[slug]`), catch-all (`[...slug]`), optional catch-all (`[[...slug]]`), route groups `(group)`, and route conflict detection.
162
162
  - **SSG, SSR, ISR** — static generation, on-demand SSR, and incremental static regeneration with explicit cache policy (`public`/`private`/`dynamic`), SHA-256 cache keys, atomic writes, single-flight, and tag-based invalidation.
163
- - **Streaming (experimental)** — `ReadableStream`-based streaming with `loading.ts` boundaries, `createStreamingResponse()`, and `createBufferedResponse()` fallback for adapters without streaming. **Etiquetado como experimental** hasta completar la matriz de paridad streaming/buffered cross-host y la implementación de `renderToChunks()` en el core.
163
+ - **Streaming (experimental)** — real streaming SSR in the unified handler: opt-in via `defineConfig({ streaming: true })`, routes with `loading.ts` stream the shell + fallback immediately and swap in the resolved content with a `<template>` chunk. See [Streaming SSR (experimental)](#streaming-ssr-experimental).
164
164
  - **Server actions** — typed `defineAction()` with input validation (`.parse()`), AbortSignal propagation, idempotency metadata, concurrency modes (`latest`/`queue`/`parallel`), and progressive enhancement (plain HTML forms).
165
165
  - **RequestContext** — per-request context with `params`, `locals`, `cookies` (CookieJar), `signal` (AbortSignal), `requestId`, `platform`, `route`, and mutable `response` state (headers, Set-Cookie, status). Aligned with runtime-security §4.
166
- - **Unified Web handler** — `createWebHandler()` is the single entry point for all runtimes (Node, Bun, Vercel, Netlify, Vite dev **y el CLI `dev`/`preview`**). Every runtime is a thin wrapper; no duplicated routing/actions/static pipelines.
166
+ - **Unified Web handler** — `createWebHandler()` is the single entry point for all runtimes (Node, Bun, Vercel, Netlify, Vite dev **y el CLI `dev`/`preview`/`start`**). Every runtime is a thin wrapper; no duplicated routing/actions/static pipelines.
167
167
  - **Cache security** — `shouldCachePublic()` rejects requests with cookies/Authorization. `isResultCacheable()` rejects HTML with action error markers. No personalized ISR cache leakage.
168
168
  - **Public error sanitization** — production 500s use `toPublicErrorInfo()`/`publicErrorResponse()` (JSON, `no-store`), never exposing stacks, paths or secrets; request id is kept in internal logs.
169
169
  - **CSRF protection** — `verifyOrigin()` checks `Origin`, `Referer`, `Host`, and `Sec-Fetch-Site` with allow-list and `strictOrigin` mode.
@@ -173,8 +173,8 @@ Options:
173
173
  - **SEO** — sitemap generation from route manifest, sitemap index for >50,000 URLs, robots.txt, JSON-LD with safe escaping (`<`, `>`, `&`, U+2028, U+2029).
174
174
  - **Image optimization** — manifest-driven `<picture>` with content-addressed hashed variants, `<source>` per format, real dimensions, no upscales, Sharp optional.
175
175
  - **Islands** — lazy `import()` per island, null/error isolation, `load`/`idle`/`visible` directives, auto-scan of `src/islands/`.
176
- - **Client router** — AbortController + navigation token (no races), head/assets merge, aria-live announcer, canonical URL, View Transitions with reduced-motion fallback.
177
- - **Middleware** — `src/middleware.ts` with path matchers, `next()` carries params/locals, cleanup in `finally`, runs in dev/preview/adapters.
176
+ - **Client router** — navigation lifecycle events (`elur:navigate-*`, `elur:before-render`), per-page JS gating (0 KB on island-free pages), `data-elur-persist` element persistence, bounded LRU prefetch cache (network-aware, `pointerdown`), `history.scrollRestoration = "manual"`, head/payload/script refresh on navigation, optional idiomorph morphing (`router.morph`), optional loading indicator, Speculation Rules (`router.speculation`), head/assets merge, aria-live announcer, canonical URL, View Transitions with reduced-motion fallback.
177
+ - **Middleware** — `src/middleware.ts` with path matchers, `next()` carries headers/locals into the request pipeline, cleanup in `finally`, runs in `dev`/`preview`/`start` (the unified Web handler).
178
178
  - **Integrations** — typed hooks for `elur-i18n`, `elur-auth`, `elur-query`, `elur-testing` without adding them as dependencies.
179
179
  - **CLI** — `dev`, `build`, `preview`, `start`, `check`, `routes`, `doctor`, `adapter` with reliable exit codes.
180
180
  - **Observability** — structured logger with request ID, Server-Timing, sensitive data redaction (cookies, auth, tokens).
@@ -183,6 +183,135 @@ Options:
183
183
  - **`throw new Response()`** — first-class HTTP control flow from loaders and layout loaders (redirects, 404, etc.).
184
184
  - **HMAC-signed action errors** — action error cookies signed with SHA-256, rejects tampered/forged values.
185
185
 
186
+ ## Streaming SSR (experimental)
187
+
188
+ Real streaming for dynamic routes, wired into the unified Web handler
189
+ (`createWebHandler`) — so it works identically in `dev`, `preview`, `start`
190
+ and the generated Node/Bun servers.
191
+
192
+ ### Enabling it
193
+
194
+ Streaming is **opt-in** (default `false` — every route renders buffered
195
+ exactly as before unless you enable it):
196
+
197
+ ```ts
198
+ // elur.config.ts
199
+ import { defineConfig } from "@elurjs/kit";
200
+
201
+ export default defineConfig({
202
+ streaming: true,
203
+ });
204
+ ```
205
+
206
+ Requirements:
207
+
208
+ - The route must define a **loading boundary** (`src/app/<route>/loading.ts`).
209
+ Routes without one render buffered, unchanged.
210
+ - The host adapter must declare `capabilities.streaming: true`. Node, Bun and
211
+ the CLI dev/preview servers do; hosts that cannot stream fall back to
212
+ buffered rendering automatically instead of breaking. The CLI `adapter`
213
+ command validates the combination at build time.
214
+
215
+ ### What you get
216
+
217
+ 1. The browser receives the **document shell + loading fallback immediately**
218
+ (low TTFB), with status 200 and `Content-Type: text/html; charset=utf-8`.
219
+ 2. The page render (loaders included) runs **in the background**.
220
+ 3. When it finishes, the server appends a `<template>` chunk plus a small
221
+ script that **swaps the loading boundary in-place** (`replaceWith`), then
222
+ dispatches `elur:rendered`. No full re-render, no client fetch.
223
+
224
+ Emitted headers on streamed responses:
225
+
226
+ | Header | Why |
227
+ | --- | --- |
228
+ | `Content-Type: text/html; charset=utf-8` | sent early so the browser parses progressively |
229
+ | `Transfer-Encoding: chunked` | no `Content-Length`; each chunk flushes as produced |
230
+ | `X-Accel-Buffering: no` | tells nginx and similar reverse proxies not to buffer the stream |
231
+ | `Cache-Control: no-store` | a half-sent dynamic stream must never be cached by intermediaries |
232
+
233
+ The usual observability headers (`X-Request-ID`, `Server-Timing`,
234
+ security headers, route headers) apply as always. Note that `Server-Timing`'s
235
+ `ssr` metric measures **time-to-shell**, not the full background render.
236
+
237
+ ### Redirects, errors and disconnects mid-stream
238
+
239
+ - A loader throwing a redirect `Response` emits a
240
+ `<script>window.location.href=…</script>` chunk — the browser navigates
241
+ even though the shell was already sent.
242
+ - A loader failure after the shell went out swaps the boundary for a sober,
243
+ inline-styled error notice (`role="alert"`) and logs the detail via
244
+ `console.error`.
245
+ - If the client disconnects, the request's `AbortSignal` cancels the stream;
246
+ a render completing late is discarded and never writes to a dead stream.
247
+ The Node server also cancels the upstream stream when the socket closes.
248
+
249
+ ### Interaction with the ISR cache
250
+
251
+ **Streamed pages bypass the ISR cache entirely** — they are not read from it
252
+ nor written to it, and render live on every request. Nobody caches a stream
253
+ mid-flight; if a route is cacheable, buffered + ISR is the better delivery
254
+ mode. Keep `streaming: false` (or remove the `loading.ts` boundary) for
255
+ routes you want cached.
256
+
257
+ ### Limitations
258
+
259
+ - **Experimental**: the streaming/buffered parity matrix across hosts is not
260
+ complete yet, and the API may change.
261
+ - The swap script is inline: a strict `Content-Security-Policy` without
262
+ `unsafe-inline` (or a nonce) blocks it. Configure CSP accordingly.
263
+ - Verified on Node (dev/preview/`start`, generated Node server) and Bun
264
+ (`Bun.serve` streams natively). The Vercel/Netlify adapters are out of
265
+ scope for now.
266
+ - Behind a reverse proxy, make sure buffering and gzip buffering are disabled
267
+ for streamed routes (the `X-Accel-Buffering: no` header covers nginx).
268
+
269
+ ## What's new in v2.5
270
+
271
+ **0% JavaScript by default + next-generation router.**
272
+
273
+ - **Per-page JS gating** — the document shell scans the rendered body
274
+ for `data-elur-island` markers: pages without islands ship **0 KB of
275
+ JS** when `router.enabled: false`, or **only the router chunk** when
276
+ the router is on. Pages with islands get `entry-client.js` +
277
+ `router.js`, each with `<link rel="modulepreload">`. Escape hatch:
278
+ `defineConfig({ js: "legacy" })` restores the unconditional combined
279
+ entry. See [Per-page JavaScript emission](#per-page-javascript-emission).
280
+ - **Split client entries** — the generated entry is hydrate-only and
281
+ the router lives in `.elur/router.ts` → `/_elur/router.js`.
282
+ Single-input `vite.client.config.*` files keep working (router
283
+ embedded = legacy mode de facto).
284
+ - **Production head fix** — `/__elur-js/render` now returns
285
+ `{ title, body, head, data, actions, clearActionErrorCookie }` in
286
+ every runtime; head metadata updates on SPA navigations and the
287
+ action-error cookie is actually cleared.
288
+ - **Navigation lifecycle events** — `elur:navigate-start` /
289
+ `elur:navigate-end` / `elur:navigate-error` with
290
+ `{ pathname, search, fromCache, popstate }`, plus
291
+ `elur:before-render` before the swap (islands dispose while still
292
+ attached; `detail.persisted` lists survivors).
293
+ - **`data-elur-persist="key"`** — live DOM nodes are moved (not
294
+ re-rendered) across navigations via `Element.moveBefore()`
295
+ (`replaceWith` fallback); islands inside keep their state.
296
+ - **Router robustness** — bounded LRU prefetch cache (32 entries,
297
+ 30 s), `history.scrollRestoration = "manual"` with per-entry scroll,
298
+ prefetch on `pointerenter`/`focus`/`pointerdown` + opt-in viewport
299
+ (`data-prefetch="viewport"`), Save-Data/2g-aware, inline `<script>`
300
+ re-execution after navigation, `#elur-data`/`#elur-actions` refresh.
301
+ - **Optional extras** — `router.morph` (idiomorph-based DOM morphing,
302
+ experimental), `router.loadingIndicator` (top progress bar, >200 ms
303
+ delay, reduced-motion aware), `router.speculation` (Speculation
304
+ Rules API on static pages).
305
+ - **`directive: "load"` is real now** — the generated entry calls
306
+ `hydrateIslands()` immediately; the global `requestIdleCallback`
307
+ wrapper is gone. `idle`/`visible` keep their deferred scheduling.
308
+ - **`startClientRouter(options)`** accepts
309
+ `{ prefetch, morph, loadingIndicator }`; `prefetch()` gains
310
+ `{ force: true }` to bypass network guards.
311
+
312
+ All changes are backwards-compatible; existing projects keep working
313
+ unchanged.
314
+
186
315
  ## What's new in v2.4
187
316
 
188
317
  - **Fixed: image pipeline silently no-op** — the CLI bundle was
@@ -232,7 +361,15 @@ Options:
232
361
  SSR errors are never silently swallowed — they propagate wrapped with
233
362
  the island name and remediation hints.
234
363
 
235
- #### Using the `build` hook for sitemaps
364
+ ### Using the `build` hook for sitemaps
365
+
366
+ When `site` is set in your config, the build **already generates
367
+ `sitemap.xml` automatically** from the scanned routes (dynamic routes,
368
+ error pages and internal namespaces excluded; sites over 50,000 URLs are
369
+ split into a sitemap index). You only need the manual hook below for full
370
+ control over the entries (per-URL `priority`/`changefreq`, dynamic-route
371
+ URLs, extra files). An existing `sitemap.xml` — from `public/` or written
372
+ by an integration — always takes precedence over the automatic one.
236
373
 
237
374
  ```ts
238
375
  // elur.config.ts
@@ -305,7 +442,7 @@ When both plugins are installed:
305
442
  | Feature | Kit only | Kit + Vite plugin |
306
443
  | --- | --- | --- |
307
444
  | Partial attr interpolation | Legacy transform (heuristic) | State-machine lexer (compile-time) |
308
- | Build-time compiler | No | Yes (`html\`\`` → imperative DOM) |
445
+ | Build-time compiler | No | Yes (``html` `` → imperative DOM) |
309
446
  | HMR state preservation | No | Yes (signals, stores, forms, routers) |
310
447
  | Scroll/focus preservation | No | Yes |
311
448
  | SSR | Works (kit handles it) | Works (plugin skips compiler in SSR) |
@@ -423,6 +560,7 @@ receive the full transform pipeline.
423
560
  | v2.4.2 | Integration `build` hook wired into `build()`, `BuildResult.outDir` for post-build artifacts ✅ |
424
561
  | v2.4.3 | Client-only islands (`directive: "only"`, `options: { ssr: false, fallback }`), `isSSR()` export, SSR error wrapping ✅ |
425
562
  | v2.4.4 | Fix: `"only"` directive now hydrates immediately like `"load"`. Fix: islands without SSR DOM use fresh `_render` mount instead of `hydrateTemplate` ✅ |
563
+ | v2.5 | 0% JS por defecto (gating por página + split entry/router + `js:"legacy"`), router de nueva generación (eventos de ciclo de vida, `data-elur-persist`, LRU+network-aware prefetch, morph opcional, speculation rules, loading indicator), fix head/payload del render endpoint en prod, `load` real ✅ |
426
564
 
427
565
  ## API
428
566
 
@@ -905,7 +1043,7 @@ export default defineConfig({
905
1043
 
906
1044
  The Vite plugin activates:
907
1045
 
908
- - **Build-time compiler** — lowers `html\`\`` templates to imperative DOM
1046
+ - **Build-time compiler** — lowers ``html` `` templates to imperative DOM
909
1047
  code (`firstChild`/`nextSibling` navigation, inline `setAttribute`,
910
1048
  grouped effects, event delegation). Eliminates `detectContext`,
911
1049
  `buildHTML`, and both `TreeWalker` passes in runtime.
@@ -1291,16 +1429,111 @@ export const config = {
1291
1429
  };
1292
1430
  ```
1293
1431
 
1294
- ### Prefetch and View Transitions
1432
+ The middleware runs in `dev`, `preview` and `start` through the unified Web
1433
+ handler: after redirects/rewrites and the internal endpoints, before routing.
1434
+ Returning a `Response` short-circuits the pipeline (security headers and
1435
+ `X-Request-ID` still apply); `next({ headers })` merges headers into the
1436
+ downstream request and `next({ locals })` is exposed to API routes as
1437
+ `ctx.locals`. The generated Node/Bun adapter servers do not run the
1438
+ middleware file yet.
1295
1439
 
1296
- The SPA router automatically prefetches pages when links enter the viewport
1297
- (IntersectionObserver) and on hover/focus. Prefetched pages are cached for
1298
- 30 seconds. Add `data-no-prefetch` to any link to opt out.
1440
+ ### Redirects, rewrites and route headers
1441
+
1442
+ Declare redirects, rewrites and extra response headers in `elur.config.ts`.
1443
+ All patterns support `:param` segments, `*` wildcards and `:name*` catch-alls,
1444
+ with `:param` interpolation in destinations:
1445
+
1446
+ ```ts
1447
+ import { defineConfig } from "@elurjs/kit";
1448
+
1449
+ export default defineConfig({
1450
+ redirects: [
1451
+ { from: "/old-blog/:slug", to: "/blog/:slug", status: 301 },
1452
+ ],
1453
+ rewrites: [
1454
+ { from: "/api/legacy/*", to: "/api/v2/*" },
1455
+ ],
1456
+ headers: [
1457
+ { path: "/admin/*", headers: { "X-Robots-Tag": "noindex" } },
1458
+ ],
1459
+ });
1460
+ ```
1461
+
1462
+ Redirects are evaluated before any routing (default status `308`). Rewrites
1463
+ transparently change the pathname used for routing — API routes, static files
1464
+ and SSR all see the rewritten path. Route headers are matched against the
1465
+ original request path and applied to the response.
1466
+
1467
+ ### Router, prefetch and navigation lifecycle
1468
+
1469
+ The SPA router prefetches pages on `pointerenter`, `focus` and `pointerdown`
1470
+ (the "tap" strategy — earlier than the click on touch devices), and on links
1471
+ that opt into viewport prefetching with `data-prefetch="viewport"`
1472
+ (IntersectionObserver, 200px root margin). Prefetched pages live in a
1473
+ bounded LRU cache (32 entries, 30 s TTL). Prefetching is skipped on
1474
+ constrained networks — `Save-Data` or `slow-2g`/`2g` effective types —
1475
+ unless a link forces it with `data-prefetch="always"`. Add
1476
+ `data-no-prefetch` to any link to opt out entirely.
1477
+
1478
+ Navigation lifecycle events (on `document`) carry
1479
+ `{ pathname, search, fromCache, popstate }`:
1480
+
1481
+ - `elur:navigate-start` — a navigation begins.
1482
+ - `elur:navigate-end` — the DOM swap completed.
1483
+ - `elur:navigate-error` — the navigation failed (e.g. render endpoint
1484
+ unavailable); the router falls back to a full page load.
1485
+ - `elur:before-render` — fires *before* the `#app` swap; the generated
1486
+ entry disposes islands here (except islands inside persisted nodes,
1487
+ listed in `detail.persisted`).
1488
+ - `elur:rendered` — fires after the swap; islands re-hydrate here.
1489
+
1490
+ Elements marked `data-elur-persist="key"` survive SPA navigations: the
1491
+ router moves the *same live DOM node* into its position in the new page
1492
+ (matched by the attribute value), using `Element.moveBefore()` when
1493
+ available — media playback, island state and scroll positions are
1494
+ preserved. Islands inside persisted subtrees are not disposed/rehydrated;
1495
+ if their incoming props differ, an `elur:persist-props-changed` event
1496
+ bubbles from the island marker.
1497
+
1498
+ Scroll is managed by the router (`history.scrollRestoration = "manual"`):
1499
+ the position is saved per history entry and restored on back/forward.
1299
1500
 
1300
1501
  When the browser supports the View Transitions API, page transitions use
1301
1502
  `document.startViewTransition()` for smooth cross-fade animations. This is
1302
1503
  automatically disabled when the user has `prefers-reduced-motion: reduce`.
1303
1504
 
1505
+ Router options (`defineConfig`):
1506
+
1507
+ ```ts
1508
+ export default defineConfig({
1509
+ router: {
1510
+ enabled: true, // SPA navigation; false → MPA + 0 KB JS on island-free pages
1511
+ prefetch: true, // link prefetching (network-aware)
1512
+ morph: false, // experimental: idiomorph DOM morphing for the swap
1513
+ loadingIndicator: false,// top progress bar on navigations slower than ~200 ms
1514
+ speculation: undefined, // "prefetch" | "prerender" → Speculation Rules on static pages
1515
+ },
1516
+ js: "modern", // "legacy" restores the unconditional combined entry
1517
+ });
1518
+ ```
1519
+
1520
+ ### Per-page JavaScript emission
1521
+
1522
+ With `js: "modern"` (default) the document shell inspects the rendered body
1523
+ and only ships what the page needs:
1524
+
1525
+ | Page contents | `router.enabled: true` | `router.enabled: false` |
1526
+ |---|---|---|
1527
+ | With islands | `entry-client.js` + `router.js` | `entry-client.js` only |
1528
+ | Without islands | `router.js` only | **0 KB of JS** |
1529
+
1530
+ The split requires the router to be its own bundle chunk — automatic when
1531
+ the project has no `vite.client.config.*` (the CLI synthesizes a default
1532
+ two-input config) or when that config declares `.elur/router.ts` as a
1533
+ second input. A single-input config keeps the router embedded in
1534
+ `entry-client.js` (emitted whenever islands exist *or* the router is on).
1535
+ Set `js: "legacy"` to restore the pre-split behavior unconditionally.
1536
+
1304
1537
  ## Project conventions
1305
1538
 
1306
1539
  ```text
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../action-DZHihU3Z.cjs");exports.callAction=e.t,exports.defineAction=e.r,exports.elurJsAction=e.n;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../action-BiE4tEty.cjs");exports.callAction=e.t,exports.defineAction=e.r,exports.elurJsAction=e.n;
@@ -1,2 +1,2 @@
1
- import { n as e, r as t, t as n } from "../action-BB6N5yD-.js";
1
+ import { n as e, r as t, t as n } from "../action-CXVk8rWt.js";
2
2
  export { n as callAction, t as defineAction, e as elurJsAction };
@@ -1,2 +1,2 @@
1
- const e=require("./errors-0X7pCFNU.cjs");let t=require("@elurjs/core");var n=globalThis.AbortController;function r(e,t){let r=e.concurrency??`latest`,a=e.idempotent??!1,o=e.invalidateTags??[],s=e.invalidatePaths??[],c=null,l=[],u=0;async function d(n,r){if(e.input)try{n=e.input.parse(n)}catch(e){let t=e instanceof Error?e.message:String(e),{fail:n}=await Promise.resolve().then(()=>require("./errors-0X7pCFNU.cjs")).then(e=>e.r);return n(400,{validation:t})}return t(n,r)}let f=async(e,t)=>{if(r===`latest`){c&&c.abort(),c=new n;let r=i(t.signal,c.signal);return d(e,{...t,signal:r})}if(r===`queue`){u>0&&await new Promise(e=>l.push(e)),u++;try{return await d(e,t)}finally{u--;let e=l.shift();e&&e()}}return d(e,t)};return f.__elurAction={name:t.name||`anonymous`,concurrency:r,idempotent:a,invalidateTags:o,invalidatePaths:s},f}function i(...e){let t=new n;for(let n of e)if(n){if(n.aborted){t.abort();break}n.addEventListener(`abort`,()=>t.abort(),{once:!0})}return t.signal}function a(e){return typeof e==`object`&&!!e&&e.__elur_js_action_failure===!0}function o(e){return typeof e==`object`&&!!e&&e.__elur_js_action_redirect===!0}async function s(t,n=[],r={}){let i=Array.isArray(n)?n:[n],s=await fetch(`/__elur-js/actions`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({name:t,page:r.page,args:i})}),c=await s.text();if(!s.ok){let n;try{n=JSON.parse(c)}catch{}if(a(n)&&n.status!==void 0)return new e.t(n.status,n.data);throw Error(`Action "${t}" failed: ${c}`)}let l=JSON.parse(c);return o(l)&&l.status!==void 0&&l.location!==void 0?new e.n(l.status,l.location):l}function c(e,n={}){let r=(0,t.signal)(!1),i=(0,t.signal)(null),a=(0,t.signal)(null);async function o(t){r.value=!0,i.value=null;try{let r=await s(e,t,n);return a.value=r,r}catch(e){throw i.value=e instanceof Error?e:Error(String(e)),e}finally{r.value=!1}}return{submit:o,pending:r,data:a,error:i}}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return s}});
2
- //# sourceMappingURL=action-DZHihU3Z.cjs.map
1
+ const e=require("./errors-kSl9yLkg.cjs");let t=require("@elurjs/core");var n=globalThis.AbortController;function r(e,t){let r=e.concurrency??`latest`,a=e.idempotent??!1,o=e.invalidateTags??[],s=e.invalidatePaths??[],c=null,l=[],u=0;async function d(n,r){if(e.input)try{n=e.input.parse(n)}catch(e){let t=e instanceof Error?e.message:String(e),{fail:n}=await Promise.resolve().then(()=>require("./errors-kSl9yLkg.cjs")).then(e=>e.r);return n(400,{validation:t})}return t(n,r)}let f=async(e,t)=>{if(r===`latest`){c&&c.abort(),c=new n;let r=i(t.signal,c.signal);return d(e,{...t,signal:r})}if(r===`queue`){u>0&&await new Promise(e=>l.push(e)),u++;try{return await d(e,t)}finally{u--;let e=l.shift();e&&e()}}return d(e,t)};return f.__elurAction={name:t.name||`anonymous`,concurrency:r,idempotent:a,invalidateTags:o,invalidatePaths:s},f}function i(...e){let t=new n;for(let n of e)if(n){if(n.aborted){t.abort();break}n.addEventListener(`abort`,()=>t.abort(),{once:!0})}return t.signal}function a(e){return typeof e==`object`&&!!e&&e.__elur_js_action_failure===!0}function o(e){return typeof e==`object`&&!!e&&e.__elur_js_action_redirect===!0}async function s(t,n=[],r={}){let i=Array.isArray(n)?n:[n],s=await fetch(`/__elur-js/actions`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({name:t,page:r.page,args:i})}),c=await s.text();if(!s.ok){let n;try{n=JSON.parse(c)}catch{}if(a(n)&&n.status!==void 0)return new e.t(n.status,n.data);throw Error(`Action "${t}" failed: ${c}`)}let l=JSON.parse(c);return o(l)&&l.status!==void 0&&l.location!==void 0?new e.n(l.status,l.location):l}function c(e,n={}){let r=(0,t.signal)(!1),i=(0,t.signal)(null),a=(0,t.signal)(null);async function o(t){r.value=!0,i.value=null;try{let r=await s(e,t,n);return a.value=r,r}catch(e){throw i.value=e instanceof Error?e:Error(String(e)),e}finally{r.value=!1}}return{submit:o,pending:r,data:a,error:i}}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return s}});
2
+ //# sourceMappingURL=action-BiE4tEty.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"action-DZHihU3Z.cjs","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"action-BiE4tEty.cjs","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { n as e, t } from "./errors-ePYxDlhU.js";
1
+ import { n as e, t } from "./errors-j4cg9VN3.js";
2
2
  import { signal as n } from "@elurjs/core";
3
3
  //#region src/action/define.ts
4
4
  var r = globalThis.AbortController;
@@ -8,7 +8,7 @@ function i(e, t) {
8
8
  if (e.input) try {
9
9
  n = e.input.parse(n);
10
10
  } catch (e) {
11
- let t = e instanceof Error ? e.message : String(e), { fail: n } = await import("./errors-ePYxDlhU.js").then((e) => e.r);
11
+ let t = e instanceof Error ? e.message : String(e), { fail: n } = await import("./errors-j4cg9VN3.js").then((e) => e.r);
12
12
  return n(400, { validation: t });
13
13
  }
14
14
  return t(n, r);
@@ -108,4 +108,4 @@ function l(e, t = {}) {
108
108
  //#endregion
109
109
  export { l as n, i as r, c as t };
110
110
 
111
- //# sourceMappingURL=action-BB6N5yD-.js.map
111
+ //# sourceMappingURL=action-CXVk8rWt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"action-BB6N5yD-.js","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"action-CXVk8rWt.js","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}