@taujs/react 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,96 +6,8 @@
6
6
 
7
7
  `pnpm add @taujs/react`
8
8
 
9
- ## CSR; SSR; Streaming SSR; Hydration; Fastify + React 19
9
+ # τjs
10
10
 
11
- Supports rendering modes:
11
+ React Renderer: CSR, SSR, Streaming SSR
12
12
 
13
- - Client-side rendering (CSR)
14
- - Server-side rendering (SSR)
15
- - Streaming SSR
16
-
17
- Supported application structure and composition:
18
-
19
- - Single-page Application (SPA)
20
- - Multi-page Application (MPA)
21
- - Build-time Micro-Frontends (MFE), with server orchestration and delivery
22
-
23
- Assemble independent frontends at build time incorporating flexible per-route SPA-MPA hybrid with CSR, SSR, and Streaming SSR, rendering options.
24
-
25
- Fastify Plugin for integration with taujs [ τjs ] template https://github.com/aoede3/taujs
26
-
27
- - Production: Fastify, React
28
- - Development: Fastify, React, tsx, Vite
29
-
30
- - TypeScript-first
31
- - ESM-only focus
32
-
33
- ## τjs - DX Developer Experience
34
-
35
- Integrated Vite HMR run alongside tsx (TS eXecute) providing fast responsive dev reload times for universal backend / frontend changes
36
-
37
- - Fastify https://fastify.dev/
38
- - React https://reactjs.org/
39
- - tsx https://tsx.is/
40
- - Vite https://vitejs.dev/guide/ssr#building-for-production
41
-
42
- - ESBuild https://esbuild.github.io/
43
- - Rollup https://rollupjs.org/
44
- - ESM https://nodejs.org/api/esm.html
45
-
46
- ## Development / CI
47
-
48
- `npm install --legacy-peer-deps`
49
-
50
- ## Usage
51
-
52
- ### Fastify
53
-
54
- https://github.com/aoede3/taujs/blob/main/src/server/index.ts
55
-
56
- Not utilising taujs [ τjs ] template? Add in your own ts `alias` object for your own particular directory setup e.g. `alias: { object }`
57
-
58
- ### React 'entry-client.tsx'
59
-
60
- https://github.com/aoede3/taujs/blob/main/src/client/entry-client.tsx
61
-
62
- ### React 'entry-server.tsx'
63
-
64
- Extended pipe object with callbacks to @taujs/server enabling additional manipulation of HEAD content from client code
65
-
66
- https://github.com/aoede3/taujs/blob/main/src/client/entry-server.tsx
67
-
68
- ### index.html
69
-
70
- https://github.com/aoede3/taujs/blob/main/src/client/index.html
71
-
72
- ### client.d.ts
73
-
74
- https://github.com/aoede3/taujs/blob/main/src/client/client.d.ts
75
-
76
- ### Routes
77
-
78
- Integral to τjs is its internal routing:
79
-
80
- 1. Fastify serving index.html to client browser for client routing
81
- 2. Internal service calls to API to provide data for streaming/hydration
82
- 3. Fastify serving API calls via HTTP in the more traditional sense of client/server
83
-
84
- In ensuring a particular 'route' receives data for hydration there are two options:
85
-
86
- 1. An HTTP call syntactically not unlike 'fetch' providing params to a 'fetch' call
87
- 2. Internal service call returning data as per your architecture
88
-
89
- In supporting Option 2. there is a registry of services. More detail in 'Service Registry'.
90
-
91
- Each routes 'path' is a simple URL regex as per below examples.
92
-
93
- https://github.com/aoede3/taujs/blob/main/src/shared/routes/Routes.ts
94
-
95
- ### Service Registry
96
-
97
- In supporting internal calls via τjs a registry of available services and methods can provide linkage to your own architectural setup and developmental patterns
98
-
99
- https://github.com/aoede3/taujs/blob/main/src/server/services/ServiceRegistry.ts
100
-
101
- https://github.com/aoede3/taujs/blob/main/src/server/services/ServiceExample.ts
13
+ https://taujs.dev/
package/dist/index.d.ts CHANGED
@@ -1,49 +1,88 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
1
2
  import React from 'react';
2
- import { ServerResponse } from 'node:http';
3
+ import { Writable } from 'node:stream';
3
4
 
4
5
  type SSRStore<T> = {
5
6
  getSnapshot: () => T;
6
7
  getServerSnapshot: () => T;
7
8
  setData: (newData: T) => void;
8
9
  subscribe: (callback: () => void) => () => void;
10
+ status: 'pending' | 'success' | 'error';
11
+ lastError?: Error;
9
12
  };
10
- declare const createSSRStore: <T>(initialDataOrPromise: T | Promise<T> | (() => Promise<T>)) => SSRStore<T>;
11
- declare const SSRStoreProvider: React.FC<React.PropsWithChildren<{
12
- store: SSRStore<Record<string, unknown>>;
13
- }>>;
13
+ declare function createSSRStore<T>(initialDataOrPromise: T | Promise<T> | (() => Promise<T>)): SSRStore<T>;
14
+ declare const SSRStoreProvider: <T>({ store, children }: React.PropsWithChildren<{
15
+ store: SSRStore<T>;
16
+ }>) => react_jsx_runtime.JSX.Element;
14
17
  declare const useSSRStore: <T>() => T;
15
18
 
16
- type HydrateAppOptions = {
19
+ type UILogger = {
20
+ log: (...args: unknown[]) => void;
21
+ warn: (...args: unknown[]) => void;
22
+ error: (...args: unknown[]) => void;
23
+ };
24
+ type ServerLogs = {
25
+ info: (message: string, meta?: unknown) => void;
26
+ warn: (message: string, meta?: unknown) => void;
27
+ error: (message: string, meta?: unknown) => void;
28
+ debug?: (category: string, message: string, meta?: unknown) => void;
29
+ child?: (ctx: Record<string, unknown>) => ServerLogs;
30
+ isDebugEnabled?: (category: string) => boolean;
31
+ };
32
+ type LoggerLike = Partial<UILogger> | Partial<ServerLogs>;
33
+
34
+ type HydrateAppOptions<T> = {
17
35
  appComponent: React.ReactElement;
18
- initialDataKey?: keyof Window;
19
36
  rootElementId?: string;
20
- debug?: boolean;
37
+ enableDebug?: boolean;
38
+ logger?: LoggerLike;
39
+ dataKey?: string;
40
+ onHydrationError?: (err: unknown) => void;
41
+ onStart?: () => void;
42
+ onSuccess?: () => void;
21
43
  };
22
- declare const hydrateApp: ({ appComponent, initialDataKey, rootElementId, debug }: HydrateAppOptions) => void;
44
+ declare function hydrateApp<T>({ appComponent, rootElementId, enableDebug, logger, dataKey, onHydrationError, onStart, onSuccess, }: HydrateAppOptions<T>): void;
23
45
 
24
- type RendererOptions = {
46
+ type RenderCallbacks<T> = {
47
+ onHead?: (head: string) => boolean | void;
48
+ onShellReady?: () => void;
49
+ onAllReady?: (data: T) => void;
50
+ onFinish?: (data: T) => void;
51
+ onError?: (err: unknown) => void;
52
+ };
53
+ type StreamOptions = {
54
+ /** Timeout in ms for shell to be ready (default: 10000) */
55
+ shellTimeoutMs?: number;
56
+ /** Whether to use cork/uncork for batched writes (default: true) */
57
+ useCork?: boolean;
58
+ };
59
+ type SSRResult = {
60
+ headContent: string;
61
+ appHtml: string;
62
+ aborted: boolean;
63
+ };
64
+ type StreamCallOptions = StreamOptions & {
65
+ logger?: LoggerLike;
66
+ };
67
+ declare function createRenderer<T extends Record<string, unknown>>({ appComponent, headContent, streamOptions, logger, enableDebug, }: {
25
68
  appComponent: (props: {
26
69
  location: string;
27
70
  }) => React.ReactElement;
28
- headContent: string | ((data: Record<string, unknown>) => string);
29
- };
30
- type RenderCallbacks = {
31
- onHead: (headContent: string) => void;
32
- onFinish: (initialDataPromise: unknown) => void;
33
- onError: (error: unknown) => void;
34
- };
35
- declare const resolveHeadContent: (headContent: string | ((meta: Record<string, unknown>) => string), meta?: Record<string, unknown>) => string;
36
- declare const createRenderer: ({ appComponent, headContent }: RendererOptions) => {
37
- renderSSR: (initialDataPromise: Record<string, unknown>, location: string, meta?: Record<string, unknown>) => Promise<{
38
- headContent: string;
39
- appHtml: string;
40
- }>;
41
- renderStream: (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Record<string, unknown>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>, cspNonce?: string) => void;
42
- };
43
- declare const createRenderStream: (serverResponse: ServerResponse, { onHead, onFinish, onError }: RenderCallbacks, { appComponent, headContent, initialDataPromise, location, bootstrapModules, }: RendererOptions & {
44
- initialDataPromise: Record<string, unknown>;
45
- location: string;
46
- bootstrapModules?: string;
47
- }, cspNonce?: string) => void;
71
+ headContent: (ctx: {
72
+ data: T;
73
+ meta: Record<string, unknown>;
74
+ }) => string;
75
+ enableDebug?: boolean;
76
+ logger?: LoggerLike;
77
+ streamOptions?: StreamOptions;
78
+ }): {
79
+ renderSSR: (initialData: T, location: string, meta?: Record<string, unknown>, signal?: AbortSignal, opts?: {
80
+ logger?: LoggerLike;
81
+ }) => Promise<SSRResult>;
82
+ renderStream: (writable: Writable, callbacks: RenderCallbacks<T> | undefined, initialData: T | Promise<T> | (() => Promise<T>), location: string, bootstrapModules?: string, meta?: Record<string, unknown>, cspNonce?: string, signal?: AbortSignal, opts?: StreamCallOptions) => {
83
+ abort: () => void;
84
+ done: Promise<void>;
85
+ };
86
+ };
48
87
 
49
- export { type SSRStore, SSRStoreProvider, createRenderStream, createRenderer, createSSRStore, hydrateApp, resolveHeadContent, useSSRStore };
88
+ export { type HydrateAppOptions, type RenderCallbacks, type SSRStore, SSRStoreProvider, type ServerLogs, type StreamOptions, createRenderer, createSSRStore, hydrateApp, useSSRStore };
package/dist/index.js CHANGED
@@ -1,31 +1,33 @@
1
1
  // src/SSRDataStore.tsx
2
- import { createContext, useContext, useSyncExternalStore } from "react";
2
+ import { createContext, useContext, useSyncExternalStore, useDeferredValue, useMemo } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
- var createSSRStore = (initialDataOrPromise) => {
4
+ function createSSRStore(initialDataOrPromise) {
5
5
  let currentData;
6
6
  let status;
7
7
  let lastError;
8
- const subscribers = /* @__PURE__ */ new Set();
9
8
  let serverDataPromise;
9
+ const subscribers = /* @__PURE__ */ new Set();
10
+ const notify = () => subscribers.forEach((cb) => cb());
10
11
  const handleError = (error) => {
11
- console.error("Failed to load initial data:", error);
12
- lastError = error instanceof Error ? error : new Error(String(JSON.stringify(error)));
12
+ const normalised = error instanceof Error ? error : new Error(String(JSON.stringify(error)));
13
+ console.error("Failed to load initial data:", normalised);
14
+ lastError = normalised;
13
15
  status = "error";
16
+ notify();
14
17
  };
15
18
  if (typeof initialDataOrPromise === "function") {
16
19
  status = "pending";
17
- const promiseFromFunction = initialDataOrPromise();
18
- serverDataPromise = promiseFromFunction.then((data) => {
20
+ serverDataPromise = initialDataOrPromise().then((data) => {
19
21
  currentData = data;
20
22
  status = "success";
21
- subscribers.forEach((callback) => callback());
23
+ notify();
22
24
  }).catch(handleError);
23
25
  } else if (initialDataOrPromise instanceof Promise) {
24
26
  status = "pending";
25
27
  serverDataPromise = initialDataOrPromise.then((data) => {
26
28
  currentData = data;
27
29
  status = "success";
28
- subscribers.forEach((callback) => callback());
30
+ notify();
29
31
  }).catch(handleError);
30
32
  } else {
31
33
  currentData = initialDataOrPromise;
@@ -35,38 +37,41 @@ var createSSRStore = (initialDataOrPromise) => {
35
37
  const setData = (newData) => {
36
38
  currentData = newData;
37
39
  status = "success";
38
- subscribers.forEach((callback) => callback());
40
+ notify();
39
41
  };
40
42
  const subscribe = (callback) => {
41
43
  subscribers.add(callback);
42
44
  return () => subscribers.delete(callback);
43
45
  };
44
46
  const getSnapshot = () => {
45
- if (status === "pending") {
46
- throw serverDataPromise;
47
- } else if (status === "error") {
48
- throw new Error(`SSR data fetch failed: ${lastError?.message || "Unknown error"}`);
49
- }
47
+ if (status === "pending") throw serverDataPromise;
48
+ if (status === "error") throw new Error(`SSR data fetch failed: ${lastError?.message || "Unknown error"}`);
50
49
  if (currentData === void 0) throw new Error("SSR data is undefined - store initialisation problem");
51
50
  return currentData;
52
51
  };
53
52
  const getServerSnapshot = () => {
54
- if (status === "pending") {
55
- throw serverDataPromise;
56
- } else if (status === "error") {
57
- throw new Error(`Server-side data fetch failed: ${lastError?.message || "Unknown error"}`);
58
- }
53
+ if (status === "pending") throw serverDataPromise;
54
+ if (status === "error") throw new Error(`Server-side data fetch failed: ${lastError?.message || "Unknown error"}`);
59
55
  if (currentData === void 0) throw new Error("Server data not available - check SSR configuration");
60
56
  return currentData;
61
57
  };
62
- return { getSnapshot, getServerSnapshot, setData, subscribe };
63
- };
58
+ return {
59
+ getSnapshot,
60
+ getServerSnapshot,
61
+ setData,
62
+ subscribe,
63
+ status,
64
+ lastError
65
+ };
66
+ }
64
67
  var SSRStoreContext = createContext(null);
65
68
  var SSRStoreProvider = ({ store, children }) => /* @__PURE__ */ jsx(SSRStoreContext.Provider, { value: store, children });
66
69
  var useSSRStore = () => {
67
70
  const store = useContext(SSRStoreContext);
68
71
  if (!store) throw new Error("useSSRStore must be used within a SSRStoreProvider");
69
- return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
72
+ const syncVal = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
73
+ const deferred = useDeferredValue(syncVal);
74
+ return useMemo(() => deferred, [deferred]);
70
75
  };
71
76
 
72
77
  // src/SSRHydration.tsx
@@ -74,127 +79,479 @@ import React2 from "react";
74
79
  import { createRoot, hydrateRoot } from "react-dom/client";
75
80
 
76
81
  // src/utils/Logger.ts
77
- var createLogger = (debug) => ({
78
- log: (...args) => {
79
- if (debug) console.log(...args);
80
- },
81
- warn: (...args) => {
82
- if (debug) console.warn(...args);
83
- },
84
- error: (...args) => {
85
- if (debug) console.error(...args);
82
+ var toJSONString = (v) => typeof v === "string" ? v : v instanceof Error ? v.stack ?? v.message : JSON.stringify(v);
83
+ var splitMsgAndMeta = (args) => {
84
+ const [first, ...rest] = args;
85
+ const msg = toJSONString(first);
86
+ if (rest.length === 0) return { msg, meta: void 0 };
87
+ const only = rest.length === 1 ? rest[0] : void 0;
88
+ const meta = only && typeof only === "object" && !(only instanceof Error) ? only : { args: rest.map(toJSONString) };
89
+ return { msg, meta };
90
+ };
91
+ function createUILogger(logger, opts = {}) {
92
+ const { debugCategory = "ssr", context, preferDebug = false, enableDebug = false } = opts;
93
+ if (!enableDebug) return { log: () => {
94
+ }, warn: () => {
95
+ }, error: () => {
96
+ } };
97
+ const looksServer = !!logger && ("info" in logger || "debug" in logger || "child" in logger || "isDebugEnabled" in logger);
98
+ if (looksServer) {
99
+ let s = logger;
100
+ if (s.child && context) {
101
+ try {
102
+ s = s.child.call(s, context);
103
+ } catch {
104
+ }
105
+ }
106
+ const info = s.info ? s.info.bind(s) : (m, meta) => meta ? console.log(m, meta) : console.log(m);
107
+ const warn = s.warn ? s.warn.bind(s) : (m, meta) => meta ? console.warn(m, meta) : console.warn(m);
108
+ const error = s.error ? s.error.bind(s) : (m, meta) => meta ? console.error(m, meta) : console.error(m);
109
+ const debug = s.debug ? s.debug.bind(s) : void 0;
110
+ const isDebugEnabled = s.isDebugEnabled ? s.isDebugEnabled.bind(s) : void 0;
111
+ return {
112
+ log: (...args) => {
113
+ const { msg, meta } = splitMsgAndMeta(args);
114
+ if (debug) {
115
+ const enabled = (isDebugEnabled ? isDebugEnabled(debugCategory) : false) || preferDebug;
116
+ if (enabled) {
117
+ debug(debugCategory, msg, meta);
118
+ return;
119
+ }
120
+ }
121
+ info(msg, meta);
122
+ },
123
+ warn: (...args) => {
124
+ const { msg, meta } = splitMsgAndMeta(args);
125
+ warn(msg, meta);
126
+ },
127
+ error: (...args) => {
128
+ const { msg, meta } = splitMsgAndMeta(args);
129
+ error(msg, meta);
130
+ }
131
+ };
86
132
  }
87
- });
133
+ const ui = logger || {};
134
+ return {
135
+ log: (...a) => (ui.log ?? console.log)(...a),
136
+ warn: (...a) => (ui.warn ?? console.warn)(...a),
137
+ error: (...a) => (ui.error ?? console.error)(...a)
138
+ };
139
+ }
88
140
 
89
141
  // src/SSRHydration.tsx
90
142
  import { jsx as jsx2 } from "react/jsx-runtime";
91
- var hydrateApp = ({ appComponent, initialDataKey = "__INITIAL_DATA__", rootElementId = "root", debug = false }) => {
92
- const { log, warn, error } = createLogger(debug);
143
+ function hydrateApp({
144
+ appComponent,
145
+ rootElementId = "root",
146
+ enableDebug = false,
147
+ logger,
148
+ dataKey = "__INITIAL_DATA__",
149
+ onHydrationError,
150
+ onStart,
151
+ onSuccess
152
+ }) {
153
+ const { log, warn, error } = createUILogger(logger, { debugCategory: "ssr", context: { scope: "react-hydration" }, enableDebug });
154
+ const mountCSR = (rootEl) => {
155
+ rootEl.innerHTML = "";
156
+ const root = createRoot(rootEl);
157
+ root.render(/* @__PURE__ */ jsx2(React2.StrictMode, { children: appComponent }));
158
+ };
159
+ const startHydration = (rootEl, initialData) => {
160
+ if (enableDebug) log("Hydration started");
161
+ onStart?.();
162
+ if (enableDebug) log("Initial data loaded:", initialData);
163
+ const store = createSSRStore(initialData);
164
+ if (enableDebug) log("Store created:", store);
165
+ try {
166
+ hydrateRoot(
167
+ rootEl,
168
+ /* @__PURE__ */ jsx2(React2.StrictMode, { children: /* @__PURE__ */ jsx2(SSRStoreProvider, { store, children: appComponent }) }),
169
+ {
170
+ onRecoverableError: (err, info) => {
171
+ warn("Recoverable hydration error:", err, info);
172
+ }
173
+ }
174
+ );
175
+ if (enableDebug) log("Hydration completed");
176
+ onSuccess?.();
177
+ } catch (err) {
178
+ error("Hydration error:", err);
179
+ onHydrationError?.(err);
180
+ }
181
+ };
93
182
  const bootstrap = () => {
94
- log("Hydration started");
95
- const rootElement = document.getElementById(rootElementId);
96
- if (!rootElement) {
183
+ const rootEl = document.getElementById(rootElementId);
184
+ if (!rootEl) {
97
185
  error(`Root element with id "${rootElementId}" not found.`);
98
186
  return;
99
187
  }
100
- const initialData = window[initialDataKey];
101
- if (!initialData) {
102
- warn(`Initial data key "${initialDataKey}" is undefined on window. Defaulting to SPA createRoot`);
103
- const root = createRoot(rootElement);
104
- root.render(/* @__PURE__ */ jsx2(React2.StrictMode, { children: appComponent }));
105
- } else {
106
- log("Initial data loaded:", initialData);
107
- const initialDataPromise = Promise.resolve(initialData);
108
- const store = createSSRStore(initialDataPromise);
109
- log("Store created:", store);
110
- hydrateRoot(
111
- rootElement,
112
- /* @__PURE__ */ jsx2(React2.StrictMode, { children: /* @__PURE__ */ jsx2(SSRStoreProvider, { store, children: appComponent }) })
113
- );
114
- log("Hydration completed");
188
+ const data = window[dataKey];
189
+ if (data === void 0) {
190
+ if (enableDebug) warn(`No initial SSR data at window["${dataKey}"]. Mounting CSR.`);
191
+ mountCSR(rootEl);
192
+ return;
115
193
  }
194
+ startHydration(rootEl, data);
116
195
  };
117
196
  if (document.readyState !== "loading") {
118
197
  bootstrap();
119
198
  } else {
120
- document.addEventListener("DOMContentLoaded", bootstrap);
199
+ document.addEventListener("DOMContentLoaded", bootstrap, { once: true });
121
200
  }
122
- };
201
+ }
123
202
 
124
203
  // src/SSRRender.tsx
125
- import "http";
126
- import { Writable } from "stream";
127
204
  import "react";
128
205
  import { renderToPipeableStream, renderToString } from "react-dom/server";
129
- import { jsx as jsx3 } from "react/jsx-runtime";
130
- var resolveHeadContent = (headContent, meta = {}) => typeof headContent === "function" ? headContent(meta) : headContent;
131
- var createRenderer = ({ appComponent, headContent }) => {
132
- const renderSSR = async (initialDataPromise, location, meta = {}) => {
133
- const dataForHeadContent = Object.keys(initialDataPromise).length > 0 ? initialDataPromise : meta;
134
- const dynamicHeadContent = resolveHeadContent(headContent, dataForHeadContent);
135
- const appHtml = renderToString(/* @__PURE__ */ jsx3(SSRStoreProvider, { store: createSSRStore(initialDataPromise), children: appComponent({ location }) }));
136
- return {
137
- headContent: dynamicHeadContent,
138
- appHtml
206
+
207
+ // src/utils/Streaming.ts
208
+ var DEFAULT_BENIGN_ERRORS = /ECONNRESET|EPIPE|socket hang up|aborted|premature/i;
209
+ function isBenignStreamErr(err) {
210
+ const msg = String(err?.message ?? "");
211
+ return DEFAULT_BENIGN_ERRORS.test(msg);
212
+ }
213
+ function createSettler() {
214
+ let settled = false;
215
+ let resolve;
216
+ let reject;
217
+ const done = new Promise((r, j) => {
218
+ resolve = () => {
219
+ if (!settled) {
220
+ settled = true;
221
+ r();
222
+ }
139
223
  };
224
+ reject = (e) => {
225
+ if (!settled) {
226
+ settled = true;
227
+ j(e);
228
+ }
229
+ };
230
+ });
231
+ return { done, resolve, reject, isSettled: () => settled };
232
+ }
233
+ function startShellTimer(ms, onTimeout) {
234
+ const t = setTimeout(onTimeout, ms);
235
+ return () => clearTimeout(t);
236
+ }
237
+ function wireWritableGuards(writable, {
238
+ benignAbort,
239
+ fatalAbort,
240
+ onError,
241
+ onFinish,
242
+ benignErrorPattern = DEFAULT_BENIGN_ERRORS
243
+ }) {
244
+ const handlers = [];
245
+ const add = (ev, fn) => {
246
+ writable.once(ev, fn);
247
+ handlers.push(() => writable.removeListener(ev, fn));
140
248
  };
141
- const renderStream = (serverResponse, callbacks, initialDataPromise, location, bootstrapModules, meta = {}, cspNonce) => {
142
- const dynamicHeadContent = resolveHeadContent(headContent, meta);
143
- createRenderStream(
144
- serverResponse,
145
- callbacks,
146
- {
147
- appComponent: (props) => appComponent({ ...props, location }),
148
- headContent: dynamicHeadContent,
149
- initialDataPromise,
150
- location,
151
- bootstrapModules
152
- },
153
- cspNonce
154
- );
249
+ add("error", (err) => {
250
+ const msg = String(err?.message ?? "");
251
+ if (benignErrorPattern.test(msg)) {
252
+ benignAbort("Client disconnected during stream");
253
+ } else {
254
+ onError?.(err);
255
+ fatalAbort(err);
256
+ }
257
+ });
258
+ add("close", () => benignAbort("Writable closed early (likely client disconnect)"));
259
+ add("finish", () => {
260
+ if (onFinish) onFinish();
261
+ else benignAbort("Stream finished (normal completion)");
262
+ });
263
+ return {
264
+ cleanup: () => {
265
+ for (const off of handlers) {
266
+ try {
267
+ off();
268
+ } catch {
269
+ }
270
+ }
271
+ }
155
272
  };
156
- return { renderSSR, renderStream };
157
- };
158
- var createRenderStream = (serverResponse, { onHead, onFinish, onError }, {
273
+ }
274
+ function createStreamController(writable, logger) {
275
+ const { log, warn, error } = logger;
276
+ let aborted = false;
277
+ const settle = createSettler();
278
+ let stopShellTimer;
279
+ let removeAbortListener;
280
+ let guardsCleanup;
281
+ let streamAbort;
282
+ const cleanup = (benign, err) => {
283
+ if (aborted) return;
284
+ aborted = true;
285
+ try {
286
+ stopShellTimer?.();
287
+ } catch {
288
+ }
289
+ try {
290
+ removeAbortListener?.();
291
+ } catch {
292
+ }
293
+ try {
294
+ guardsCleanup?.();
295
+ } catch {
296
+ }
297
+ try {
298
+ streamAbort?.();
299
+ } catch {
300
+ }
301
+ try {
302
+ if (!writable.writableEnded && !writable.destroyed) writable.destroy();
303
+ } catch {
304
+ }
305
+ if (benign) settle.resolve();
306
+ else if (err !== void 0) settle.reject(err);
307
+ else settle.resolve();
308
+ };
309
+ return {
310
+ setStreamAbort: (fn) => {
311
+ streamAbort = fn;
312
+ },
313
+ setStopShellTimer: (fn) => {
314
+ stopShellTimer = fn;
315
+ },
316
+ setRemoveAbortListener: (fn) => {
317
+ removeAbortListener = fn;
318
+ },
319
+ setGuardsCleanup: (fn) => {
320
+ guardsCleanup = fn;
321
+ },
322
+ complete(message) {
323
+ if (aborted) return;
324
+ if (message) (log ?? warn)(message);
325
+ cleanup(true);
326
+ },
327
+ benignAbort(why) {
328
+ if (aborted) return;
329
+ warn(why);
330
+ cleanup(true);
331
+ },
332
+ fatalAbort(err) {
333
+ if (aborted) return;
334
+ error("Stream aborted with error:", err);
335
+ cleanup(false, err);
336
+ },
337
+ get done() {
338
+ return settle.done;
339
+ },
340
+ get isAborted() {
341
+ return aborted;
342
+ }
343
+ };
344
+ }
345
+
346
+ // src/SSRRender.tsx
347
+ import { jsx as jsx3 } from "react/jsx-runtime";
348
+ function createRenderer({
159
349
  appComponent,
160
350
  headContent,
161
- initialDataPromise,
162
- location,
163
- bootstrapModules
164
- }, cspNonce) => {
165
- const store = createSSRStore(initialDataPromise);
166
- const appElement = /* @__PURE__ */ jsx3(SSRStoreProvider, { store, children: appComponent({ location }) });
167
- const { pipe } = renderToPipeableStream(appElement, {
168
- nonce: cspNonce,
169
- bootstrapModules: bootstrapModules ? [bootstrapModules] : void 0,
170
- onShellReady() {
171
- const dynamicHeadContent = resolveHeadContent(headContent, initialDataPromise);
172
- onHead(dynamicHeadContent);
173
- pipe(
174
- new Writable({
175
- write(chunk, _encoding, callback) {
176
- serverResponse.write(chunk, callback);
177
- },
178
- final(callback) {
179
- onFinish(store.getSnapshot());
180
- callback();
351
+ streamOptions = {},
352
+ logger,
353
+ enableDebug = false
354
+ }) {
355
+ const { shellTimeoutMs = 1e4, useCork = true } = streamOptions;
356
+ const renderSSR = async (initialData, location, meta = {}, signal, opts) => {
357
+ const { log, warn } = createUILogger(opts?.logger ?? logger, {
358
+ debugCategory: "ssr",
359
+ context: { scope: "react-ssr" },
360
+ enableDebug
361
+ });
362
+ if (signal?.aborted) {
363
+ warn("SSR skipped; already aborted", { location });
364
+ return { headContent: "", appHtml: "", aborted: true };
365
+ }
366
+ let aborted = false;
367
+ const onAbort = () => aborted = true;
368
+ signal?.addEventListener("abort", onAbort, { once: true });
369
+ try {
370
+ log("Starting SSR:", location);
371
+ const dynamicHead = headContent({ data: initialData, meta });
372
+ const store = createSSRStore(initialData);
373
+ const html = renderToString(/* @__PURE__ */ jsx3(SSRStoreProvider, { store, children: appComponent({ location }) }));
374
+ if (aborted) {
375
+ warn("SSR completed after client abort", { location });
376
+ return { headContent: "", appHtml: "", aborted: true };
377
+ }
378
+ log("Completed SSR:", location);
379
+ return { headContent: dynamicHead, appHtml: html, aborted: false };
380
+ } finally {
381
+ try {
382
+ signal?.removeEventListener("abort", onAbort);
383
+ } catch {
384
+ }
385
+ }
386
+ };
387
+ const renderStream = (writable, callbacks = {}, initialData, location, bootstrapModules, meta = {}, cspNonce, signal, opts) => {
388
+ const { onAllReady, onError, onHead, onShellReady, onFinish } = callbacks;
389
+ const { log, warn, error } = createUILogger(opts?.logger ?? logger, {
390
+ debugCategory: "ssr",
391
+ context: { scope: "react-streaming" },
392
+ enableDebug
393
+ });
394
+ const effectiveShellTimeout = opts?.shellTimeoutMs ?? shellTimeoutMs;
395
+ const effectiveUseCork = opts?.useCork ?? useCork;
396
+ const controller = createStreamController(writable, { log, warn, error });
397
+ if (signal) {
398
+ const handleAbortSignal = () => controller.benignAbort(`AbortSignal triggered; aborting stream for location: ${location}`);
399
+ if (signal.aborted) {
400
+ handleAbortSignal();
401
+ return { abort: () => {
402
+ }, done: Promise.resolve() };
403
+ }
404
+ signal.addEventListener("abort", handleAbortSignal, { once: true });
405
+ controller.setRemoveAbortListener(() => {
406
+ try {
407
+ signal.removeEventListener("abort", handleAbortSignal);
408
+ } catch {
409
+ }
410
+ });
411
+ }
412
+ const { cleanup: guardsCleanup } = wireWritableGuards(writable, {
413
+ benignAbort: (why) => controller.benignAbort(why),
414
+ fatalAbort: (err) => {
415
+ onError?.(err);
416
+ controller.fatalAbort(err);
417
+ },
418
+ onError,
419
+ onFinish: () => controller.complete("Stream finished (normal completion)")
420
+ });
421
+ controller.setGuardsCleanup(guardsCleanup);
422
+ const stopShellTimer = startShellTimer(effectiveShellTimeout, () => {
423
+ if (controller.isAborted) return;
424
+ const timeoutErr = new Error(`Shell not ready after ${effectiveShellTimeout}ms`);
425
+ onError?.(timeoutErr);
426
+ controller.fatalAbort(timeoutErr);
427
+ });
428
+ controller.setStopShellTimer(stopShellTimer);
429
+ log("Starting stream:", location);
430
+ try {
431
+ const store = createSSRStore(initialData);
432
+ const appElement = /* @__PURE__ */ jsx3(SSRStoreProvider, { store, children: appComponent({ location }) });
433
+ const stream = renderToPipeableStream(appElement, {
434
+ nonce: cspNonce,
435
+ bootstrapModules: bootstrapModules ? [bootstrapModules] : void 0,
436
+ onShellReady() {
437
+ if (controller.isAborted) return;
438
+ try {
439
+ stopShellTimer();
440
+ } catch {
181
441
  }
182
- })
183
- );
184
- },
185
- onAllReady() {
186
- },
187
- onError(error) {
188
- onError(error);
442
+ log("Shell ready:", location);
443
+ try {
444
+ let headData;
445
+ try {
446
+ headData = store.getSnapshot();
447
+ } catch (thrown) {
448
+ if (thrown && typeof thrown.then === "function") {
449
+ thrown.catch(() => {
450
+ });
451
+ }
452
+ }
453
+ const head = headContent({ data: headData ?? {}, meta });
454
+ const canCork = effectiveUseCork && typeof writable?.cork === "function" && typeof writable?.uncork === "function";
455
+ if (canCork) {
456
+ try {
457
+ writable.cork();
458
+ } catch {
459
+ }
460
+ }
461
+ let wroteOk = true;
462
+ try {
463
+ const res = writable?.write ? writable.write(head) : true;
464
+ wroteOk = res !== false;
465
+ } finally {
466
+ if (canCork) {
467
+ try {
468
+ writable.uncork();
469
+ } catch {
470
+ }
471
+ }
472
+ }
473
+ let forceWait = false;
474
+ try {
475
+ const ret = onHead?.(head);
476
+ forceWait = ret === false;
477
+ } catch (cbErr) {
478
+ warn("onHead callback threw:", cbErr);
479
+ }
480
+ const startPipe = () => stream.pipe(writable);
481
+ if (forceWait || !wroteOk) writable?.once?.("drain", startPipe);
482
+ else startPipe();
483
+ try {
484
+ onShellReady?.();
485
+ } catch (cbErr) {
486
+ warn("onShellReady callback threw:", cbErr);
487
+ }
488
+ } catch (err) {
489
+ onError?.(err);
490
+ controller.fatalAbort(err);
491
+ }
492
+ },
493
+ onAllReady() {
494
+ if (controller.isAborted) return;
495
+ log("All content ready:", location);
496
+ const deliver = () => {
497
+ try {
498
+ const data = store.getSnapshot();
499
+ onAllReady?.(data);
500
+ onFinish?.(data);
501
+ } catch (thrown) {
502
+ if (thrown && typeof thrown.then === "function") {
503
+ thrown.then(deliver).catch((e) => {
504
+ error("Data promise rejected:", e);
505
+ onError?.(e);
506
+ controller.fatalAbort(e);
507
+ });
508
+ } else {
509
+ error("Unexpected throw from getSnapshot:", thrown);
510
+ onError?.(thrown);
511
+ controller.fatalAbort(thrown);
512
+ }
513
+ }
514
+ };
515
+ deliver();
516
+ },
517
+ onShellError(err) {
518
+ if (controller.isAborted) return;
519
+ try {
520
+ stopShellTimer();
521
+ } catch {
522
+ }
523
+ onError?.(err);
524
+ controller.fatalAbort(err);
525
+ },
526
+ onError(err) {
527
+ if (controller.isAborted) return;
528
+ const msg = String(err?.message ?? "");
529
+ warn?.("React stream error:", msg);
530
+ if (isBenignStreamErr(err)) {
531
+ controller.benignAbort("Client disconnected before stream finished");
532
+ return;
533
+ }
534
+ onError?.(err);
535
+ controller.fatalAbort(err);
536
+ }
537
+ });
538
+ controller.setStreamAbort(() => stream.abort());
539
+ } catch (err) {
540
+ onError?.(err);
541
+ controller.fatalAbort(err);
189
542
  }
190
- });
191
- };
543
+ return {
544
+ abort: () => controller.benignAbort(`Manual abort for location: ${location}`),
545
+ done: controller.done
546
+ // resolves on success/benign cancel; rejects on fatal error
547
+ };
548
+ };
549
+ return { renderSSR, renderStream };
550
+ }
192
551
  export {
193
552
  SSRStoreProvider,
194
- createRenderStream,
195
553
  createRenderer,
196
554
  createSSRStore,
197
555
  hydrateApp,
198
- resolveHeadContent,
199
556
  useSSRStore
200
557
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taujs/react",
3
- "version": "0.0.8",
3
+ "version": "0.1.0",
4
4
  "description": "taujs | τjs",
5
5
  "author": "Aoede <taujs@aoede.uk.net> (https://www.aoede.uk.net)",
6
6
  "license": "MIT",
@@ -38,7 +38,6 @@
38
38
  "files": [
39
39
  "dist"
40
40
  ],
41
- "dependencies": {},
42
41
  "devDependencies": {
43
42
  "@arethetypeswrong/cli": "^0.15.4",
44
43
  "@babel/preset-typescript": "^7.24.7",
@@ -48,24 +47,23 @@
48
47
  "@types/node": "^24.0.7",
49
48
  "@types/react": "^19.0.2",
50
49
  "@types/react-dom": "^19.0.2",
51
- "@vitest/coverage-v8": "^2.1.0",
52
- "@vitest/ui": "^2.1.9",
50
+ "@vitest/coverage-v8": "^3.2.4",
51
+ "@vitest/ui": "^3.2.4",
53
52
  "jsdom": "^25.0.0",
54
53
  "prettier": "^3.3.3",
55
54
  "react": "^19.0.0",
56
55
  "react-dom": "^19.0.0",
57
56
  "tsup": "^8.2.4",
58
57
  "typescript": "^5.5.4",
59
- "vite": "^6.3.5",
60
- "vitest": "^2.0.5"
58
+ "vite": "^7.1.9",
59
+ "vitest": "^3.2.4"
61
60
  },
62
61
  "peerDependencies": {
63
- "@taujs/server": "^0.3.0",
64
62
  "@vitejs/plugin-react": "^4.6.0",
65
63
  "react": "^19.0.0",
66
64
  "react-dom": "^19.0.0",
67
65
  "typescript": "^5.5.4",
68
- "vite": "^6.3.5"
66
+ "vite": "^7.1.9"
69
67
  },
70
68
  "peerDependenciesMeta": {
71
69
  "react": {