hono-preact 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/iso/action-result-context.d.ts +22 -0
  2. package/dist/iso/action-result-context.js +2 -0
  3. package/dist/iso/action.d.ts +52 -13
  4. package/dist/iso/action.js +204 -88
  5. package/dist/iso/cache.d.ts +9 -0
  6. package/dist/iso/cache.js +26 -0
  7. package/dist/iso/define-app.d.ts +7 -0
  8. package/dist/iso/define-loader.d.ts +12 -0
  9. package/dist/iso/define-loader.js +26 -16
  10. package/dist/iso/form.d.ts +13 -4
  11. package/dist/iso/form.js +115 -33
  12. package/dist/iso/index.d.ts +13 -4
  13. package/dist/iso/index.js +14 -2
  14. package/dist/iso/internal/action-envelope.d.ts +37 -0
  15. package/dist/iso/internal/action-envelope.js +47 -0
  16. package/dist/iso/internal/action-result-store.d.ts +28 -0
  17. package/dist/iso/internal/action-result-store.js +35 -0
  18. package/dist/iso/internal/envelope.js +1 -2
  19. package/dist/iso/internal/form-submit-store.d.ts +9 -0
  20. package/dist/iso/internal/form-submit-store.js +32 -0
  21. package/dist/iso/internal/history-shim.d.ts +7 -0
  22. package/dist/iso/internal/history-shim.js +79 -0
  23. package/dist/iso/internal/loader-fetch.js +65 -34
  24. package/dist/iso/internal/loader.d.ts +3 -3
  25. package/dist/iso/internal/merge-refs.d.ts +4 -0
  26. package/dist/iso/internal/merge-refs.js +14 -0
  27. package/dist/iso/internal/persist-registry.d.ts +10 -0
  28. package/dist/iso/internal/persist-registry.js +24 -0
  29. package/dist/iso/internal/route-boundary.d.ts +4 -4
  30. package/dist/iso/internal/route-change.d.ts +8 -2
  31. package/dist/iso/internal/route-change.js +107 -12
  32. package/dist/iso/internal/safe-redirect.d.ts +7 -0
  33. package/dist/iso/internal/safe-redirect.js +27 -0
  34. package/dist/iso/internal/sse-decoder.d.ts +1 -1
  35. package/dist/iso/internal/sse-decoder.js +40 -26
  36. package/dist/iso/internal/use-render.d.ts +11 -0
  37. package/dist/iso/internal/use-render.js +47 -0
  38. package/dist/iso/internal/view-transition-event.d.ts +23 -0
  39. package/dist/iso/internal/view-transition-event.js +25 -0
  40. package/dist/iso/internal.d.ts +12 -1
  41. package/dist/iso/internal.js +13 -1
  42. package/dist/iso/optimistic-action.d.ts +10 -1
  43. package/dist/iso/optimistic-action.js +11 -3
  44. package/dist/iso/optimistic.d.ts +10 -1
  45. package/dist/iso/optimistic.js +45 -5
  46. package/dist/iso/outcomes.d.ts +14 -2
  47. package/dist/iso/outcomes.js +14 -3
  48. package/dist/iso/persist.d.ts +14 -0
  49. package/dist/iso/persist.js +56 -0
  50. package/dist/iso/use-action-result.d.ts +25 -0
  51. package/dist/iso/use-action-result.js +39 -0
  52. package/dist/iso/use-form-status.d.ts +5 -0
  53. package/dist/iso/use-form-status.js +13 -0
  54. package/dist/iso/view-transition-lifecycle.d.ts +9 -0
  55. package/dist/iso/view-transition-lifecycle.js +18 -0
  56. package/dist/iso/view-transition-name.d.ts +17 -0
  57. package/dist/iso/view-transition-name.js +79 -0
  58. package/dist/iso/view-transition-types.d.ts +8 -0
  59. package/dist/iso/view-transition-types.js +21 -0
  60. package/dist/server/actions-handler.d.ts +7 -0
  61. package/dist/server/actions-handler.js +42 -9
  62. package/dist/server/index.d.ts +2 -1
  63. package/dist/server/index.js +2 -1
  64. package/dist/server/loaders-handler.d.ts +8 -0
  65. package/dist/server/loaders-handler.js +37 -4
  66. package/dist/server/page-action-handler.d.ts +63 -0
  67. package/dist/server/page-action-handler.js +274 -0
  68. package/dist/server/page-action-resolvers.d.ts +28 -0
  69. package/dist/server/page-action-resolvers.js +147 -0
  70. package/dist/server/render.js +136 -55
  71. package/dist/server/route-server-modules.d.ts +7 -8
  72. package/dist/server/route-server-modules.js +7 -8
  73. package/dist/server/speculation-rules.d.ts +3 -0
  74. package/dist/server/speculation-rules.js +8 -0
  75. package/dist/server/sse.d.ts +43 -28
  76. package/dist/server/sse.js +113 -88
  77. package/dist/vite/client-entry.js +12 -3
  78. package/dist/vite/server-entry.js +10 -2
  79. package/package.json +2 -2
@@ -1,130 +1,155 @@
1
- import { streamSSE } from 'hono/streaming';
2
1
  import { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from '../iso/internal.js';
2
+ function sseEncodeTransform() {
3
+ const encoder = new TextEncoder();
4
+ return new TransformStream({
5
+ transform(frame, controller) {
6
+ const lines = [];
7
+ if (frame.event)
8
+ lines.push(`event: ${frame.event}`);
9
+ if (frame.id)
10
+ lines.push(`id: ${frame.id}`);
11
+ lines.push(`data: ${frame.data}`);
12
+ controller.enqueue(encoder.encode(lines.join('\n') + '\n\n'));
13
+ },
14
+ });
15
+ }
16
+ function isTimeoutAbort(signal) {
17
+ return Boolean(signal?.aborted &&
18
+ signal.reason instanceof DOMException &&
19
+ signal.reason.name === 'TimeoutError');
20
+ }
3
21
  function encodeErrorPayload(err) {
4
22
  const message = err instanceof Error ? err.message : String(err);
5
23
  const name = err instanceof Error ? err.name : 'Error';
6
24
  return JSON.stringify({ message, name });
7
25
  }
8
26
  /**
9
- * Wrap an async generator as an SSE response.
10
- *
11
- * Each yield is JSON-encoded and written as a `data:` event.
12
- * If `emitResult` is true and the generator's return value is defined,
13
- * it is written as `event: result\ndata: <json>` before the stream closes.
14
- * If the generator throws, an `event: error\ndata: {"message","name"}` frame
15
- * is written and the stream closes cleanly (Hono's default error handler is
16
- * never invoked because we catch inside the callback).
27
+ * Adapt a `ReadableStream<T>` as an async generator (with no return value).
28
+ * The reader is released in `finally`, which fires either when the consumer
29
+ * stops iterating or when the source is exhausted.
30
+ */
31
+ async function* iterReadable(stream) {
32
+ const reader = stream.getReader();
33
+ try {
34
+ while (true) {
35
+ const { done, value } = await reader.read();
36
+ if (done)
37
+ return;
38
+ yield value;
39
+ }
40
+ }
41
+ finally {
42
+ reader.cancel().catch(() => undefined);
43
+ }
44
+ }
45
+ /**
46
+ * The shared pump implementation. Iterates `source` (a generator that may
47
+ * return a final value), encodes each yielded value as a JSON `data:` frame,
48
+ * runs the observer lifecycle, and translates errors into `event: error` or
49
+ * `event: timeout` frames.
17
50
  *
18
- * When `observers` is provided, the pump fires the corresponding lifecycle
19
- * hooks (`onStart` / `onChunk` / `onEnd` / `onError` / `onAbort`) so
20
- * users can attach instrumentation via `defineStreamObserver(...)`.
51
+ * Observer state (`chunks`, `started`, `finished`) lives in the outer
52
+ * function scope so the outer ReadableStream's `cancel()` callback can fire
53
+ * `onAbort` when the consumer cancels mid-stream.
21
54
  */
22
- export function sseGeneratorResponse(c, gen, options = {}) {
23
- const { emitResult = false, observers, observerCtx } = options;
55
+ function buildSseResponse(source, options) {
56
+ const { emitResult = false, observers, observerCtx, signal, timeoutMs, } = options;
24
57
  const obs = observers ?? [];
25
- return streamSSE(c, async (stream) => {
26
- let chunks = 0;
27
- let started = false;
58
+ let chunks = 0;
59
+ let started = false;
60
+ let finished = false;
61
+ async function* framePump() {
28
62
  if (obs.length > 0 && observerCtx) {
29
63
  fanStart(obs, observerCtx);
30
64
  started = true;
31
65
  }
32
66
  try {
33
- while (!stream.aborted) {
34
- const step = await gen.next();
67
+ while (true) {
68
+ const step = await source.next();
35
69
  if (step.done) {
36
70
  if (emitResult && step.value !== undefined) {
37
- await stream.writeSSE({
38
- event: 'result',
39
- data: JSON.stringify(step.value),
40
- });
71
+ yield { event: 'result', data: JSON.stringify(step.value) };
41
72
  }
42
73
  if (started && observerCtx) {
43
74
  fanEnd(obs, observerCtx, { chunks, result: step.value });
44
75
  }
76
+ finished = true;
45
77
  return;
46
78
  }
47
- await stream.writeSSE({ data: JSON.stringify(step.value) });
79
+ yield { data: JSON.stringify(step.value) };
48
80
  if (started && observerCtx) {
49
81
  fanChunk(obs, observerCtx, step.value, chunks);
50
82
  }
51
83
  chunks += 1;
52
84
  }
53
- // Loop exited because the response stream was aborted (typically a
54
- // client disconnect). Release the generator and notify observers.
55
- await gen.return(undefined).catch(() => {
56
- /* swallow */
57
- });
58
- if (started && observerCtx) {
59
- fanAbort(obs, observerCtx, { chunks });
60
- }
61
85
  }
62
86
  catch (err) {
63
- await gen.return(undefined).catch(() => {
64
- /* swallow */
65
- });
87
+ await source.return(undefined).catch(() => undefined);
66
88
  if (started && observerCtx) {
67
89
  fanError(obs, observerCtx, err, { chunks });
68
90
  }
69
- await stream.writeSSE({
70
- event: 'error',
71
- data: encodeErrorPayload(err),
72
- });
91
+ if (isTimeoutAbort(signal) && typeof timeoutMs === 'number') {
92
+ yield { event: 'timeout', data: JSON.stringify({ timeoutMs }) };
93
+ }
94
+ else {
95
+ yield { event: 'error', data: encodeErrorPayload(err) };
96
+ }
97
+ finished = true;
73
98
  }
99
+ }
100
+ const pump = framePump();
101
+ const body = new ReadableStream({
102
+ async pull(controller) {
103
+ const { value, done } = await pump.next();
104
+ if (done) {
105
+ controller.close();
106
+ }
107
+ else {
108
+ controller.enqueue(value);
109
+ }
110
+ },
111
+ cancel() {
112
+ // Consumer cancelled before the pump completed. Notify observers via
113
+ // `onAbort` exactly when we've genuinely been aborted mid-stream
114
+ // (i.e. the source started but didn't finish naturally).
115
+ if (!finished && started && observerCtx) {
116
+ fanAbort(obs, observerCtx, { chunks });
117
+ }
118
+ pump.return(undefined).catch(() => undefined);
119
+ source.return(undefined).catch(() => undefined);
120
+ },
121
+ }).pipeThrough(sseEncodeTransform());
122
+ return new Response(body, {
123
+ headers: {
124
+ 'content-type': 'text/event-stream',
125
+ 'cache-control': 'no-cache',
126
+ },
74
127
  });
75
128
  }
76
129
  /**
77
- * Wrap a ReadableStream<T> (with T a JSON-encodable value) as an SSE response.
78
- * Each enqueued chunk is JSON-encoded and written as a `data:` event.
130
+ * Wrap an async generator as an SSE response.
79
131
  *
80
- * Observer fanout mirrors `sseGeneratorResponse`: `onStart` fires before the
81
- * first read, `onChunk` per chunk, `onEnd` on normal completion, `onError` on
82
- * throw, `onAbort` when the response stream is aborted.
132
+ * Each yield is JSON-encoded and written as a `data:` event. If `emitResult`
133
+ * is true and the generator's return value is defined, it is written as
134
+ * `event: result\ndata: <json>` before the stream closes. If the generator
135
+ * throws, an `event: error` or `event: timeout` frame is written and the
136
+ * stream closes cleanly. Observer lifecycle hooks (`onStart` / `onChunk` /
137
+ * `onEnd` / `onError` / `onAbort`) fire from inside the pump.
83
138
  */
84
- export function sseReadableStreamResponse(c, source, options = {}) {
85
- const { observers, observerCtx } = options;
86
- const obs = observers ?? [];
87
- return streamSSE(c, async (stream) => {
88
- const reader = source.getReader();
89
- let chunks = 0;
90
- let started = false;
91
- if (obs.length > 0 && observerCtx) {
92
- fanStart(obs, observerCtx);
93
- started = true;
94
- }
95
- try {
96
- while (!stream.aborted) {
97
- const { done, value } = await reader.read();
98
- if (done) {
99
- if (started && observerCtx) {
100
- fanEnd(obs, observerCtx, { chunks, result: undefined });
101
- }
102
- return;
103
- }
104
- await stream.writeSSE({ data: JSON.stringify(value) });
105
- if (started && observerCtx) {
106
- fanChunk(obs, observerCtx, value, chunks);
107
- }
108
- chunks += 1;
109
- }
110
- if (started && observerCtx) {
111
- fanAbort(obs, observerCtx, { chunks });
112
- }
113
- }
114
- catch (err) {
115
- if (started && observerCtx) {
116
- fanError(obs, observerCtx, err, { chunks });
117
- }
118
- await stream.writeSSE({
119
- event: 'error',
120
- data: encodeErrorPayload(err),
121
- });
122
- }
123
- finally {
124
- reader.cancel().catch(() => {
125
- /* swallow */
126
- });
127
- }
139
+ export function sseGeneratorResponse(_c, gen, options = {}) {
140
+ return buildSseResponse(gen, options);
141
+ }
142
+ /**
143
+ * Wrap a `ReadableStream<T>` (with `T` a JSON-encodable value) as an SSE
144
+ * response. Each enqueued chunk is JSON-encoded and written as a `data:`
145
+ * event. Observer lifecycle hooks fire identically to `sseGeneratorResponse`;
146
+ * `emitResult` is not meaningful here (streams have no return value) and is
147
+ * ignored.
148
+ */
149
+ export function sseReadableStreamResponse(_c, source, options = {}) {
150
+ return buildSseResponse(iterReadable(source), {
151
+ ...options,
152
+ emitResult: false,
128
153
  });
129
154
  }
130
155
  export function isAsyncGenerator(value) {
@@ -2,14 +2,23 @@ import * as path from 'node:path';
2
2
  export const VIRTUAL_CLIENT_ENTRY_ID = 'virtual:hono-preact/client';
3
3
  const RESOLVED_ID = '\0' + VIRTUAL_CLIENT_ENTRY_ID;
4
4
  export function generateClientEntrySource(opts) {
5
- return (`import { h, hydrate } from 'preact';\n` +
5
+ return (`import { h, hydrate, render as renderPreact } from 'preact';\n` +
6
6
  `import { LocationProvider } from 'preact-iso';\n` +
7
- `import { Routes } from 'hono-preact';\n` +
8
- `import { __dispatchRouteChange, installStreamRegistry } from 'hono-preact/internal';\n` +
7
+ `import { Routes, PersistHost } from 'hono-preact';\n` +
8
+ `import { __dispatchRouteChange, installStreamRegistry, installHistoryShim } from 'hono-preact/internal';\n` +
9
9
  `import routes from '${opts.routesAbsPath}';\n` +
10
10
  `\n` +
11
+ `installHistoryShim();\n` +
11
12
  `installStreamRegistry();\n` +
12
13
  `\n` +
14
+ `let persistHost = document.getElementById('__hp_persist_root');\n` +
15
+ `if (!persistHost) {\n` +
16
+ ` persistHost = document.createElement('div');\n` +
17
+ ` persistHost.id = '__hp_persist_root';\n` +
18
+ ` document.body.appendChild(persistHost);\n` +
19
+ `}\n` +
20
+ `renderPreact(h(PersistHost, null), persistHost);\n` +
21
+ `\n` +
13
22
  `let lastPath;\n` +
14
23
  `function onRouteChange(path) {\n` +
15
24
  ` const from = lastPath;\n` +
@@ -22,9 +22,10 @@ export function generateCoreAppModule(opts) {
22
22
  `import { LocationProvider } from 'preact-iso';\n` +
23
23
  `import { Routes, env } from 'hono-preact';\n` +
24
24
  `import {\n` +
25
- ` actionsHandler,\n` +
26
25
  ` loadersHandler,\n` +
26
+ ` makePageActionResolvers,\n` +
27
27
  ` makePageUseResolvers,\n` +
28
+ ` pageActionHandler,\n` +
28
29
  ` renderPage,\n` +
29
30
  ` routeServerModules,\n` +
30
31
  `} from 'hono-preact/server';\n` +
@@ -37,11 +38,18 @@ export function generateCoreAppModule(opts) {
37
38
  `const dev = import.meta.env.DEV;\n` +
38
39
  `const serverModules = routeServerModules(routes);\n` +
39
40
  `const pageUseResolvers = makePageUseResolvers(routes.serverRoutes, { dev });\n` +
41
+ `const pageActionResolvers = makePageActionResolvers(routes.serverRoutes, { dev });\n` +
40
42
  `\n` +
41
43
  `export const app = new Hono()\n` +
42
44
  apiMount +
43
45
  ` .post('/__loaders', loadersHandler(serverModules, { dev, appConfig, resolvePageUse: pageUseResolvers.byPath }))\n` +
44
- ` .post('/__actions', actionsHandler(serverModules, { dev, appConfig, resolvePageUse: pageUseResolvers.byModuleKey }))\n` +
46
+ ` .post('*', pageActionHandler({\n` +
47
+ ` resolverByPath: pageActionResolvers.byPath,\n` +
48
+ ` resolvePageUseByPath: pageUseResolvers.byPath,\n` +
49
+ ` renderPage,\n` +
50
+ ` resolvePageNode: () => h(Layout, null, h(LocationProvider, null, h(Routes, { routes }))),\n` +
51
+ ` appConfig,\n` +
52
+ ` }))\n` +
45
53
  ` .get('*', (c) => renderPage(c, h(Layout, null, h(LocationProvider, null, h(Routes, { routes }))), { appConfig }));\n` +
46
54
  `\n` +
47
55
  `export default app;\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-preact",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Hono on the edge, Preact in the browser, manifest driven routes, typed RPC, streaming everywhere.",
5
5
  "keywords": [
6
6
  "hono",
@@ -95,8 +95,8 @@
95
95
  },
96
96
  "devDependencies": {
97
97
  "typescript": "*",
98
- "@hono-preact/iso": "0.1.0",
99
98
  "@hono-preact/server": "0.1.0",
99
+ "@hono-preact/iso": "0.1.0",
100
100
  "@hono-preact/vite": "0.1.0"
101
101
  },
102
102
  "scripts": {