@fluojs/react 0.1.0 → 0.2.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 (59) hide show
  1. package/README.ko.md +629 -16
  2. package/README.md +644 -25
  3. package/dist/decorators.d.ts +3 -3
  4. package/dist/decorators.d.ts.map +1 -1
  5. package/dist/decorators.js +10 -4
  6. package/dist/diagnostics.d.ts +110 -0
  7. package/dist/diagnostics.d.ts.map +1 -0
  8. package/dist/diagnostics.js +180 -0
  9. package/dist/error-representation.d.ts +28 -0
  10. package/dist/error-representation.d.ts.map +1 -0
  11. package/dist/error-representation.js +28 -0
  12. package/dist/experimental/rsc-flight-response.d.ts.map +1 -1
  13. package/dist/experimental/rsc-flight-response.js +2 -6
  14. package/dist/experimental/server-functions-server.js +1 -1
  15. package/dist/experimental/server-functions-types.d.ts +1 -1
  16. package/dist/experimental/server-functions-types.d.ts.map +1 -1
  17. package/dist/index.d.ts +16 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +7 -1
  20. package/dist/module.d.ts +8 -2
  21. package/dist/module.d.ts.map +1 -1
  22. package/dist/module.js +31 -8
  23. package/dist/page-catalog.d.ts +29 -0
  24. package/dist/page-catalog.d.ts.map +1 -0
  25. package/dist/page-catalog.js +46 -0
  26. package/dist/page-metadata.d.ts +58 -0
  27. package/dist/page-metadata.d.ts.map +1 -0
  28. package/dist/page-metadata.js +139 -0
  29. package/dist/page-renderer.d.ts +24 -0
  30. package/dist/page-renderer.d.ts.map +1 -0
  31. package/dist/page-renderer.js +18 -0
  32. package/dist/page-result.d.ts +16 -0
  33. package/dist/page-result.d.ts.map +1 -0
  34. package/dist/page-result.js +101 -0
  35. package/dist/render-diagnostics.d.ts +25 -0
  36. package/dist/render-diagnostics.d.ts.map +1 -0
  37. package/dist/render-diagnostics.js +94 -0
  38. package/dist/render-policy-metadata.d.ts +40 -0
  39. package/dist/render-policy-metadata.d.ts.map +1 -0
  40. package/dist/render-policy-metadata.js +136 -0
  41. package/dist/render-policy.d.ts +87 -0
  42. package/dist/render-policy.d.ts.map +1 -0
  43. package/dist/render-policy.js +203 -0
  44. package/dist/render-stream.d.ts +1 -1
  45. package/dist/render-stream.d.ts.map +1 -1
  46. package/dist/render-stream.js +80 -15
  47. package/dist/render.d.ts +12 -3
  48. package/dist/render.d.ts.map +1 -1
  49. package/dist/render.js +70 -76
  50. package/dist/server-entry.d.ts +13 -1
  51. package/dist/server-entry.d.ts.map +1 -1
  52. package/dist/server-entry.js +21 -11
  53. package/dist/typegen-artifact.d.ts +9 -0
  54. package/dist/typegen-artifact.d.ts.map +1 -0
  55. package/dist/typegen-artifact.js +116 -0
  56. package/dist/typegen.d.ts +41 -0
  57. package/dist/typegen.d.ts.map +1 -0
  58. package/dist/typegen.js +162 -0
  59. package/package.json +11 -6
@@ -0,0 +1,136 @@
1
+ import { getOwnStandardConstructorMetadataBag } from '@fluojs/core/internal';
2
+
3
+ /** Class-or-method decorator shape used by React render policy declarations. */
4
+
5
+ /** Internal render policy record retained until bootstrap validation. */
6
+
7
+ /** One class or method decoration site in a router inheritance chain. */
8
+
9
+ const reactRenderPolicyMetadataKey = Symbol.for('fluo.react.render-policy');
10
+ const legacyClassPolicyStore = new WeakMap();
11
+ const legacyMethodPolicyStore = new WeakMap();
12
+ function isStandardDecoratorContext(value) {
13
+ return typeof value === 'object' && value !== null && Reflect.get(value, 'kind') !== undefined;
14
+ }
15
+ function isClassDecoratorContext(value) {
16
+ return isStandardDecoratorContext(value) && value.kind === 'class';
17
+ }
18
+ function isMethodDecoratorContext(value) {
19
+ return isStandardDecoratorContext(value) && value.kind === 'method';
20
+ }
21
+ function isMetadataPropertyKey(value) {
22
+ return typeof value === 'string' || typeof value === 'symbol';
23
+ }
24
+ function isRenderPolicyStore(value) {
25
+ return typeof value === 'object' && value !== null && Array.isArray(Reflect.get(value, 'classRecords')) && Reflect.get(value, 'methodRecords') instanceof Map;
26
+ }
27
+ function getMetadataObject(metadata) {
28
+ return typeof metadata === 'object' && metadata !== null ? metadata : {};
29
+ }
30
+ function getStandardPolicyStoreForWrite(metadata) {
31
+ const bag = getMetadataObject(metadata);
32
+ const current = Object.hasOwn(bag, reactRenderPolicyMetadataKey) ? Reflect.get(bag, reactRenderPolicyMetadataKey) : undefined;
33
+ if (isRenderPolicyStore(current)) {
34
+ return current;
35
+ }
36
+ const created = {
37
+ classRecords: [],
38
+ methodRecords: new Map()
39
+ };
40
+ Reflect.set(bag, reactRenderPolicyMetadataKey, created);
41
+ return created;
42
+ }
43
+ function appendMethodRecord(methodRecords, propertyKey, record) {
44
+ methodRecords.set(propertyKey, [...(methodRecords.get(propertyKey) ?? []), record]);
45
+ }
46
+ function getConstructorChain(routerToken) {
47
+ const constructors = [];
48
+ let current = routerToken;
49
+ while (typeof current === 'function' && current !== Function.prototype) {
50
+ constructors.unshift(current);
51
+ current = Object.getPrototypeOf(current);
52
+ }
53
+ return constructors;
54
+ }
55
+ function getOwnStandardPolicyStore(routerToken) {
56
+ const value = getOwnStandardConstructorMetadataBag(routerToken)?.[reactRenderPolicyMetadataKey];
57
+ return isRenderPolicyStore(value) ? value : undefined;
58
+ }
59
+ function getOwnClassPolicyRecords(routerToken) {
60
+ return [...(getOwnStandardPolicyStore(routerToken)?.classRecords ?? []), ...(legacyClassPolicyStore.get(routerToken) ?? [])];
61
+ }
62
+ function getLegacyMethodRecords(routerToken) {
63
+ const prototype = Reflect.get(routerToken, 'prototype');
64
+ return typeof prototype === 'object' && prototype !== null ? legacyMethodPolicyStore.get(prototype) : undefined;
65
+ }
66
+
67
+ /**
68
+ * Creates a render policy decorator that records one component reference at a class or method site.
69
+ *
70
+ * @param kind Render policy kind being recorded.
71
+ * @param reference Component reference retained for bootstrap validation and renderer consumption.
72
+ * @returns A standard/legacy-compatible class-or-method decorator.
73
+ */
74
+ export function createReactRenderPolicyDecorator(kind, reference) {
75
+ const record = {
76
+ kind,
77
+ reference
78
+ };
79
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
80
+ if (isClassDecoratorContext(contextOrPropertyKey)) {
81
+ getStandardPolicyStoreForWrite(contextOrPropertyKey.metadata).classRecords.push(record);
82
+ return;
83
+ }
84
+ if (isMethodDecoratorContext(contextOrPropertyKey)) {
85
+ appendMethodRecord(getStandardPolicyStoreForWrite(contextOrPropertyKey.metadata).methodRecords, contextOrPropertyKey.name, record);
86
+ return;
87
+ }
88
+ if (typeof valueOrTarget === 'function' && contextOrPropertyKey === undefined) {
89
+ legacyClassPolicyStore.set(valueOrTarget, [...(legacyClassPolicyStore.get(valueOrTarget) ?? []), record]);
90
+ return;
91
+ }
92
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
93
+ let methodRecords = legacyMethodPolicyStore.get(valueOrTarget);
94
+ if (methodRecords === undefined) {
95
+ methodRecords = new Map();
96
+ legacyMethodPolicyStore.set(valueOrTarget, methodRecords);
97
+ }
98
+ appendMethodRecord(methodRecords, contextOrPropertyKey, record);
99
+ }
100
+ };
101
+ return decorator;
102
+ }
103
+
104
+ /**
105
+ * Collects own class and method policy sites across a router inheritance chain.
106
+ *
107
+ * @param routerToken Router constructor whose inherited policy declarations should be collected.
108
+ * @returns Class sites followed by method sites, each ordered base-to-derived.
109
+ */
110
+ export function getReactRenderPolicySites(routerToken) {
111
+ const constructors = getConstructorChain(routerToken);
112
+ const classSites = [];
113
+ const methodSites = [];
114
+ for (const constructor of constructors) {
115
+ const classRecords = getOwnClassPolicyRecords(constructor);
116
+ if (classRecords.length > 0) {
117
+ classSites.push({
118
+ kind: 'class',
119
+ owner: constructor,
120
+ records: classRecords
121
+ });
122
+ }
123
+ const standardMethods = getOwnStandardPolicyStore(constructor)?.methodRecords;
124
+ const legacyMethods = getLegacyMethodRecords(constructor);
125
+ const propertyKeys = new Set([...(standardMethods?.keys() ?? []), ...(legacyMethods?.keys() ?? [])]);
126
+ for (const propertyKey of propertyKeys) {
127
+ methodSites.push({
128
+ kind: 'method',
129
+ owner: constructor,
130
+ propertyKey,
131
+ records: [...(standardMethods?.get(propertyKey) ?? []), ...(legacyMethods?.get(propertyKey) ?? [])]
132
+ });
133
+ }
134
+ }
135
+ return [...classSites, ...methodSites];
136
+ }
@@ -0,0 +1,87 @@
1
+ import type { MetadataPropertyKey } from '@fluojs/core';
2
+ import type { ComponentType, ReactNode } from 'react';
3
+ import type { ReactPageMetadataFactory } from './page-metadata.js';
4
+ import type { ReactRenderContext } from './render.js';
5
+ import { type ReactRenderPolicyDecorator } from './render-policy-metadata.js';
6
+ /** Stable bootstrap diagnostic codes for invalid React render policy declarations. */
7
+ export declare const REACT_RENDER_POLICY_DIAGNOSTIC_CODES: {
8
+ readonly duplicatePageLayout: "react-render-policy-duplicate-page-layout";
9
+ readonly duplicatePageMetadata: "react-render-policy-duplicate-page-metadata";
10
+ readonly duplicateSuspenseFallback: "react-render-policy-duplicate-suspense-fallback";
11
+ readonly invalidReference: "react-render-policy-invalid-reference";
12
+ readonly invalidTarget: "react-render-policy-invalid-target";
13
+ readonly missingPageRenderer: "react-render-policy-missing-page-renderer";
14
+ };
15
+ /** Stable code attached to a React render policy bootstrap diagnostic. */
16
+ export type ReactRenderPolicyDiagnosticCode = (typeof REACT_RENDER_POLICY_DIAGNOSTIC_CODES)[keyof typeof REACT_RENDER_POLICY_DIAGNOSTIC_CODES];
17
+ /** Props supplied to a page layout component by an application page renderer. */
18
+ export type ReactPageLayoutProps = {
19
+ /** Page or nested layout content composed at this policy level. */
20
+ readonly children: ReactNode;
21
+ /** Active request-scoped React render context. */
22
+ readonly context: ReactRenderContext;
23
+ };
24
+ /** React component reference used to compose one page layout policy. */
25
+ export type ReactPageLayout = ComponentType<ReactPageLayoutProps>;
26
+ /** Props supplied to an SSR Suspense fallback component by an application page renderer. */
27
+ export type ReactSuspenseFallbackProps = {
28
+ /** Active request-scoped React render context. */
29
+ readonly context: ReactRenderContext;
30
+ };
31
+ /** React component reference used as an SSR Suspense fallback. */
32
+ export type ReactSuspenseFallback = ComponentType<ReactSuspenseFallbackProps>;
33
+ /** Resolved render policies passed to the configured application page renderer. */
34
+ export type ReactRenderPolicies = {
35
+ /** Layout references ordered from outermost class policy to innermost method policy. */
36
+ readonly layouts: readonly ReactPageLayout[];
37
+ /** Page metadata factories ordered from broadest class policy to nearest method policy. */
38
+ readonly pageMetadata?: readonly ReactPageMetadataFactory[];
39
+ /** Nearest method- or class-level SSR Suspense fallback reference. */
40
+ readonly suspenseFallback?: ReactSuspenseFallback;
41
+ };
42
+ /** Typed bootstrap failure for an invalid React render policy declaration. */
43
+ export declare class ReactRenderPolicyConfigurationError extends Error {
44
+ readonly name = "ReactRenderPolicyConfigurationError";
45
+ /** Stable machine-readable diagnostic code. */
46
+ readonly code: ReactRenderPolicyDiagnosticCode;
47
+ /** Class or class-method label that owns the invalid declaration. */
48
+ readonly target: string;
49
+ /**
50
+ * Creates a render policy bootstrap diagnostic.
51
+ *
52
+ * @param code Stable diagnostic code for the invalid declaration.
53
+ * @param target Class or class-method label that owns the declaration.
54
+ */
55
+ constructor(code: ReactRenderPolicyDiagnosticCode, target: string);
56
+ }
57
+ /**
58
+ * Marks a React router class or `@Path(...)` method with one layout component reference.
59
+ *
60
+ * @param layout Layout component reference consumed by the configured application page renderer.
61
+ * @returns A class-or-method decorator that records render-only metadata without changing HTTP matching.
62
+ */
63
+ export declare function PageLayout(layout: ReactPageLayout): ReactRenderPolicyDecorator;
64
+ /**
65
+ * Marks a React router class or `@Path(...)` method with one SSR Suspense fallback component reference.
66
+ *
67
+ * @param fallback Fallback component reference consumed by the configured application page renderer.
68
+ * @returns A class-or-method decorator that records render-only metadata without changing HTTP matching.
69
+ */
70
+ export declare function SuspenseFallback(fallback: ReactSuspenseFallback): ReactRenderPolicyDecorator;
71
+ /**
72
+ * Resolves inherited class and method render policies for one React page handler.
73
+ *
74
+ * @param routerToken React router class containing the matched page handler.
75
+ * @param propertyKey Matched `@Path(...)` method property key.
76
+ * @returns A defensive renderer-facing policy snapshot in outer-to-inner composition order.
77
+ */
78
+ export declare function getReactRenderPolicies(routerToken: Function, propertyKey: MetadataPropertyKey): ReactRenderPolicies;
79
+ /**
80
+ * Validates registered router policy declarations before request dispatch begins.
81
+ *
82
+ * @param routerTokens React module controller tokens inspected at bootstrap.
83
+ * @param hasPageRenderer Whether the module registered an application page renderer.
84
+ * @returns Nothing when every declaration is valid.
85
+ */
86
+ export declare function validateReactRenderPolicyControllers(routerTokens: readonly Function[], hasPageRenderer: boolean): void;
87
+ //# sourceMappingURL=render-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-policy.d.ts","sourceRoot":"","sources":["../src/render-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAGL,KAAK,0BAA0B,EAGhC,MAAM,6BAA6B,CAAC;AAErC,sFAAsF;AACtF,eAAO,MAAM,oCAAoC;;;;;;;CAOvC,CAAC;AAEX,0EAA0E;AAC1E,MAAM,MAAM,+BAA+B,GAAG,CAC5C,OAAO,oCAAoC,CAC5C,CAAC,MAAM,OAAO,oCAAoC,CAAC,CAAC;AAErD,iFAAiF;AACjF,MAAM,MAAM,oBAAoB,GAAG;IACjC,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;CACtC,CAAC;AAEF,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,aAAa,CAAC,oBAAoB,CAAC,CAAC;AAElE,4FAA4F;AAC5F,MAAM,MAAM,0BAA0B,GAAG;IACvC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;CACtC,CAAC;AAEF,kEAAkE;AAClE,MAAM,MAAM,qBAAqB,GAAG,aAAa,CAAC,0BAA0B,CAAC,CAAC;AAE9E,mFAAmF;AACnF,MAAM,MAAM,mBAAmB,GAAG;IAChC,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,wBAAwB,EAAE,CAAC;IAC5D,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CACnD,CAAC;AAEF,8EAA8E;AAC9E,qBAAa,mCAAoC,SAAQ,KAAK;IAC5D,SAAkB,IAAI,yCAAyC;IAE/D,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAC;IAE/C,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;gBACS,IAAI,EAAE,+BAA+B,EAAE,MAAM,EAAE,MAAM;CAKlE;AAiHD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,GAAG,0BAA0B,CAE9E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,0BAA0B,CAE5F;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,QAAQ,EACrB,WAAW,EAAE,mBAAmB,GAC/B,mBAAmB,CA2BrB;AAED;;;;;;GAMG;AACH,wBAAgB,oCAAoC,CAClD,YAAY,EAAE,SAAS,QAAQ,EAAE,EACjC,eAAe,EAAE,OAAO,GACvB,IAAI,CAoBN"}
@@ -0,0 +1,203 @@
1
+ import { getReactPathMetadata, getReactRouterMetadata } from './decorators.js';
2
+ import { createReactRenderPolicyDecorator, getReactRenderPolicySites } from './render-policy-metadata.js';
3
+
4
+ /** Stable bootstrap diagnostic codes for invalid React render policy declarations. */
5
+ export const REACT_RENDER_POLICY_DIAGNOSTIC_CODES = {
6
+ duplicatePageLayout: 'react-render-policy-duplicate-page-layout',
7
+ duplicatePageMetadata: 'react-render-policy-duplicate-page-metadata',
8
+ duplicateSuspenseFallback: 'react-render-policy-duplicate-suspense-fallback',
9
+ invalidReference: 'react-render-policy-invalid-reference',
10
+ invalidTarget: 'react-render-policy-invalid-target',
11
+ missingPageRenderer: 'react-render-policy-missing-page-renderer'
12
+ };
13
+
14
+ /** Stable code attached to a React render policy bootstrap diagnostic. */
15
+
16
+ /** Props supplied to a page layout component by an application page renderer. */
17
+
18
+ /** React component reference used to compose one page layout policy. */
19
+
20
+ /** Props supplied to an SSR Suspense fallback component by an application page renderer. */
21
+
22
+ /** React component reference used as an SSR Suspense fallback. */
23
+
24
+ /** Resolved render policies passed to the configured application page renderer. */
25
+
26
+ /** Typed bootstrap failure for an invalid React render policy declaration. */
27
+ export class ReactRenderPolicyConfigurationError extends Error {
28
+ name = 'ReactRenderPolicyConfigurationError';
29
+
30
+ /** Stable machine-readable diagnostic code. */
31
+ code;
32
+
33
+ /** Class or class-method label that owns the invalid declaration. */
34
+ target;
35
+
36
+ /**
37
+ * Creates a render policy bootstrap diagnostic.
38
+ *
39
+ * @param code Stable diagnostic code for the invalid declaration.
40
+ * @param target Class or class-method label that owns the declaration.
41
+ */
42
+ constructor(code, target) {
43
+ super(`${code}: ${target}`);
44
+ this.code = code;
45
+ this.target = target;
46
+ }
47
+ }
48
+ function siteTarget(site) {
49
+ const className = site.owner.name || '<anonymous>';
50
+ return site.kind === 'class' ? className : `${className}.${String(site.propertyKey)}`;
51
+ }
52
+ function recordsForKind(records, kind) {
53
+ return records.filter(record => record.kind === kind);
54
+ }
55
+ function isPageLayoutReference(value) {
56
+ return typeof value === 'function';
57
+ }
58
+ function isPageMetadataFactory(value) {
59
+ return typeof value === 'function';
60
+ }
61
+ function isSuspenseFallbackReference(value) {
62
+ return typeof value === 'function';
63
+ }
64
+ function readPageLayout(site) {
65
+ const records = recordsForKind(site.records, 'layout');
66
+ if (records.length > 1) {
67
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.duplicatePageLayout, siteTarget(site));
68
+ }
69
+ const [record] = records;
70
+ if (record === undefined) {
71
+ return undefined;
72
+ }
73
+ const {
74
+ reference
75
+ } = record;
76
+ if (!isPageLayoutReference(reference)) {
77
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.invalidReference, siteTarget(site));
78
+ }
79
+ return reference;
80
+ }
81
+ function readPageMetadataFactory(site) {
82
+ const records = recordsForKind(site.records, 'page-metadata');
83
+ if (records.length > 1) {
84
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.duplicatePageMetadata, siteTarget(site));
85
+ }
86
+ const [record] = records;
87
+ if (record === undefined) {
88
+ return undefined;
89
+ }
90
+ const {
91
+ reference
92
+ } = record;
93
+ if (!isPageMetadataFactory(reference)) {
94
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.invalidReference, siteTarget(site));
95
+ }
96
+ return reference;
97
+ }
98
+ function readSuspenseFallback(site) {
99
+ const records = recordsForKind(site.records, 'suspense-fallback');
100
+ if (records.length > 1) {
101
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.duplicateSuspenseFallback, siteTarget(site));
102
+ }
103
+ const [record] = records;
104
+ if (record === undefined) {
105
+ return undefined;
106
+ }
107
+ const {
108
+ reference
109
+ } = record;
110
+ if (!isSuspenseFallbackReference(reference)) {
111
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.invalidReference, siteTarget(site));
112
+ }
113
+ return reference;
114
+ }
115
+ function assertValidPolicyTarget(site) {
116
+ const valid = site.kind === 'class' ? getReactRouterMetadata(site.owner) !== undefined : getReactPathMetadata(site.owner, site.propertyKey) !== undefined;
117
+ if (!valid) {
118
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.invalidTarget, siteTarget(site));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Marks a React router class or `@Path(...)` method with one layout component reference.
124
+ *
125
+ * @param layout Layout component reference consumed by the configured application page renderer.
126
+ * @returns A class-or-method decorator that records render-only metadata without changing HTTP matching.
127
+ */
128
+ export function PageLayout(layout) {
129
+ return createReactRenderPolicyDecorator('layout', layout);
130
+ }
131
+
132
+ /**
133
+ * Marks a React router class or `@Path(...)` method with one SSR Suspense fallback component reference.
134
+ *
135
+ * @param fallback Fallback component reference consumed by the configured application page renderer.
136
+ * @returns A class-or-method decorator that records render-only metadata without changing HTTP matching.
137
+ */
138
+ export function SuspenseFallback(fallback) {
139
+ return createReactRenderPolicyDecorator('suspense-fallback', fallback);
140
+ }
141
+
142
+ /**
143
+ * Resolves inherited class and method render policies for one React page handler.
144
+ *
145
+ * @param routerToken React router class containing the matched page handler.
146
+ * @param propertyKey Matched `@Path(...)` method property key.
147
+ * @returns A defensive renderer-facing policy snapshot in outer-to-inner composition order.
148
+ */
149
+ export function getReactRenderPolicies(routerToken, propertyKey) {
150
+ const sites = getReactRenderPolicySites(routerToken);
151
+ const relevantSites = sites.filter(site => site.kind === 'class' || site.propertyKey === propertyKey);
152
+ const layouts = [];
153
+ const pageMetadata = [];
154
+ let suspenseFallback;
155
+ for (const site of relevantSites) {
156
+ const layout = readPageLayout(site);
157
+ const metadataFactory = readPageMetadataFactory(site);
158
+ const fallback = readSuspenseFallback(site);
159
+ if (layout !== undefined) {
160
+ layouts.push(layout);
161
+ }
162
+ if (metadataFactory !== undefined) {
163
+ pageMetadata.push(metadataFactory);
164
+ }
165
+ if (fallback !== undefined) {
166
+ suspenseFallback = fallback;
167
+ }
168
+ }
169
+ return {
170
+ layouts,
171
+ ...(pageMetadata.length === 0 ? {} : {
172
+ pageMetadata
173
+ }),
174
+ ...(suspenseFallback === undefined ? {} : {
175
+ suspenseFallback
176
+ })
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Validates registered router policy declarations before request dispatch begins.
182
+ *
183
+ * @param routerTokens React module controller tokens inspected at bootstrap.
184
+ * @param hasPageRenderer Whether the module registered an application page renderer.
185
+ * @returns Nothing when every declaration is valid.
186
+ */
187
+ export function validateReactRenderPolicyControllers(routerTokens, hasPageRenderer) {
188
+ for (const routerToken of routerTokens) {
189
+ const sites = getReactRenderPolicySites(routerToken);
190
+ if (sites.length === 0) {
191
+ continue;
192
+ }
193
+ if (!hasPageRenderer) {
194
+ throw new ReactRenderPolicyConfigurationError(REACT_RENDER_POLICY_DIAGNOSTIC_CODES.missingPageRenderer, routerToken.name || '<anonymous>');
195
+ }
196
+ for (const site of sites) {
197
+ assertValidPolicyTarget(site);
198
+ readPageLayout(site);
199
+ readPageMetadataFactory(site);
200
+ readSuspenseFallback(site);
201
+ }
202
+ }
203
+ }
@@ -1,4 +1,4 @@
1
- import { type FrameworkRequest, type FrameworkResponseStream } from '@fluojs/http';
1
+ import { type FrameworkRequest, type FrameworkResponseStream } from '@fluojs/http/portable';
2
2
  /** Minimal request abort surface used while React Web Streams are read. */
3
3
  export type ReactAbortSource = Pick<FrameworkRequest, 'isAborted' | 'signal'>;
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"render-stream.d.ts","sourceRoot":"","sources":["../src/render-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAOxG,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;AA+B9E;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAIzE;AAyBD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,EAAE,gBAAgB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAuCrB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,EAAE,uBAAuB,EAC/B,WAAW,EAAE,gBAAgB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAgCf"}
1
+ {"version":3,"file":"render-stream.d.ts","sourceRoot":"","sources":["../src/render-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAuB,MAAM,uBAAuB,CAAC;AAejH,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;AAuD9E;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAIzE;AAyDD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,EAAE,gBAAgB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAuCrB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,EAAE,uBAAuB,EAC/B,WAAW,EAAE,gBAAgB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA+Df"}
@@ -1,4 +1,4 @@
1
- import { RequestAbortedError } from '@fluojs/http';
1
+ import { RequestAbortedError } from '@fluojs/http/portable';
2
2
 
3
3
  /** Minimal request abort surface used while React Web Streams are read. */
4
4
 
@@ -28,6 +28,24 @@ function createAbortWait(signal) {
28
28
  promise
29
29
  };
30
30
  }
31
+ function createResponseCloseObserver(target) {
32
+ let closeObserved = target.closed;
33
+ let removeListener;
34
+ const promise = closeObserved ? Promise.resolve('closed') : target.onClose ? new Promise(resolve => {
35
+ removeListener = target.onClose?.(() => {
36
+ closeObserved = true;
37
+ resolve('closed');
38
+ }) ?? undefined;
39
+ }) : undefined;
40
+ return {
41
+ cleanup: () => {
42
+ removeListener?.();
43
+ removeListener = undefined;
44
+ },
45
+ isClosed: () => closeObserved || target.closed,
46
+ promise
47
+ };
48
+ }
31
49
  function isReactRequestAborted(source) {
32
50
  return source.isAborted?.() === true || source.signal?.aborted === true;
33
51
  }
@@ -42,20 +60,39 @@ export function throwIfReactRequestAborted(source) {
42
60
  throw new RequestAbortedError();
43
61
  }
44
62
  }
45
- async function readNextChunk(reader, abortSource) {
63
+ async function readNextChunk(reader, abortSource, responseClose) {
46
64
  if (isReactRequestAborted(abortSource)) {
47
65
  return 'aborted';
48
66
  }
67
+ if (responseClose?.isClosed()) {
68
+ return 'closed';
69
+ }
70
+ const stops = [];
49
71
  const abort = createAbortWait(abortSource.signal);
50
- if (!abort) {
72
+ if (abort) {
73
+ stops.push(abort.promise);
74
+ }
75
+ if (responseClose?.promise) {
76
+ stops.push(responseClose.promise);
77
+ }
78
+ if (stops.length === 0) {
51
79
  const next = await reader.read();
52
- return isReactRequestAborted(abortSource) ? 'aborted' : next;
80
+ if (isReactRequestAborted(abortSource)) {
81
+ return 'aborted';
82
+ }
83
+ return responseClose?.isClosed() ? 'closed' : next;
53
84
  }
54
85
  try {
55
- const next = await Promise.race([reader.read(), abort.promise]);
56
- return next === 'aborted' || isReactRequestAborted(abortSource) ? 'aborted' : next;
86
+ const next = await Promise.race([reader.read(), ...stops]);
87
+ if (next === 'aborted' || isReactRequestAborted(abortSource)) {
88
+ return 'aborted';
89
+ }
90
+ if (next === 'closed' || responseClose?.isClosed()) {
91
+ return 'closed';
92
+ }
93
+ return next;
57
94
  } finally {
58
- abort.cleanup();
95
+ abort?.cleanup();
59
96
  }
60
97
  }
61
98
 
@@ -109,24 +146,52 @@ export async function collectReadableStream(stream, abortSource) {
109
146
  */
110
147
  export async function pipeReadableStream(stream, target, abortSource) {
111
148
  const reader = stream.getReader();
149
+ let responseClose;
150
+ let cancelPromise;
151
+ let sourceCompleted = false;
152
+ let stopped = false;
153
+ const cancelReaderOnce = () => {
154
+ cancelPromise ??= reader.cancel();
155
+ return cancelPromise;
156
+ };
112
157
  try {
113
- while (!isReactRequestAborted(abortSource) && !target.closed) {
114
- const next = await readNextChunk(reader, abortSource);
115
- if (next === 'aborted') {
158
+ responseClose = createResponseCloseObserver(target);
159
+ while (!isReactRequestAborted(abortSource) && !responseClose.isClosed()) {
160
+ const next = await readNextChunk(reader, abortSource, responseClose);
161
+ if (next === 'aborted' || next === 'closed') {
162
+ stopped = true;
116
163
  break;
117
164
  }
118
165
  if (next.done) {
166
+ sourceCompleted = true;
119
167
  break;
120
168
  }
121
- const accepted = target.write(next.value);
122
- if (!accepted) {
123
- await target.waitForDrain?.();
169
+ try {
170
+ const accepted = target.write(next.value);
171
+ if (responseClose.isClosed()) {
172
+ stopped = true;
173
+ break;
174
+ }
175
+ if (!accepted) {
176
+ const drain = target.waitForDrain?.();
177
+ if (drain) {
178
+ await (responseClose.promise ? Promise.race([drain, responseClose.promise]) : drain);
179
+ }
180
+ if (responseClose.isClosed()) {
181
+ stopped = true;
182
+ break;
183
+ }
184
+ }
185
+ } catch (error) {
186
+ await Promise.allSettled([cancelReaderOnce()]);
187
+ throw error;
124
188
  }
125
189
  }
126
- if (isReactRequestAborted(abortSource)) {
127
- await reader.cancel();
190
+ if (!sourceCompleted && (stopped || isReactRequestAborted(abortSource) || responseClose.isClosed())) {
191
+ await cancelReaderOnce();
128
192
  }
129
193
  } finally {
194
+ responseClose?.cleanup();
130
195
  reader.releaseLock();
131
196
  if (!target.closed) {
132
197
  target.close();
package/dist/render.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RequestContext } from '@fluojs/http';
1
+ import type { RequestContext } from '@fluojs/http/portable';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { ReactAssetMap, ReactBootstrapAsset, ReactServerEntry } from './server-entry.js';
4
4
  /** Readable stream returned by React's Web Streams SSR renderer. */
@@ -34,8 +34,17 @@ export type RenderReactResponseOptions = {
34
34
  /** Test or custom renderer override. Defaults to lazy `react-dom/server` Web Streams rendering. */
35
35
  readonly renderToReadableStream?: ReactReadableStreamRenderer;
36
36
  };
37
- /** Minimal fluo request context needed to render one React HTML response. */
38
- export type ReactRenderContext = Pick<RequestContext, 'request' | 'requestId' | 'response'>;
37
+ /** Request-scoped fluo context available to page renderers and render policy components. */
38
+ export type ReactRenderContext = Pick<RequestContext, 'container' | 'request' | 'requestId' | 'response'>;
39
+ /**
40
+ * Buffers a React server entry without mutating or committing a framework response.
41
+ *
42
+ * @param entry React server entry whose node and hydration assets are rendered.
43
+ * @param requestContext HTTP-owned request and abort surfaces.
44
+ * @param renderToReadableStream Optional Web Streams renderer override.
45
+ * @returns Complete HTML bytes after the render finishes without errors.
46
+ */
47
+ export declare function renderReactServerEntryToBytes(entry: ReactServerEntry, requestContext: Pick<ReactRenderContext, 'request'>, renderToReadableStream?: ReactReadableStreamRenderer): Promise<Uint8Array>;
39
48
  /**
40
49
  * Renders a React server entry to one fluo HTML response using Web Streams SSR.
41
50
  *
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EACV,aAAa,EACb,mBAAmB,EAGnB,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAK3B,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,UAAU,CAAC,GAAG;IAC7D,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,gCAAgC,GAAG;IAC7C,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC,mFAAmF;IACnF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3D,4FAA4F;IAC5F,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,oFAAoF;IACpF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3D,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjE,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,CAAC;AAoBF,4EAA4E;AAC5E,MAAM,MAAM,2BAA2B,GAAG,CACxC,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,gCAAgC,KACtC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAElC,kEAAkE;AAClE,MAAM,MAAM,0BAA0B,GAAG;IACvC,iGAAiG;IACjG,QAAQ,CAAC,4BAA4B,CAAC,EAAE,MAAM,IAAI,CAAC;IACnD,mGAAmG;IACnG,QAAQ,CAAC,sBAAsB,CAAC,EAAE,2BAA2B,CAAC;CAC/D,CAAC;AAEF,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC,CAAC;AAyJ5F;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,gBAAgB,EACvB,cAAc,EAAE,kBAAkB,EAClC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,KAAK,EACV,aAAa,EACb,mBAAmB,EAEnB,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAI3B,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,UAAU,CAAC,GAAG;IAC7D,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,gCAAgC,GAAG;IAC7C,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC,mFAAmF;IACnF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3D,4FAA4F;IAC5F,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,oFAAoF;IACpF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3D,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjE,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,CAAC;AAoBF,4EAA4E;AAC5E,MAAM,MAAM,2BAA2B,GAAG,CACxC,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,gCAAgC,KACtC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAElC,kEAAkE;AAClE,MAAM,MAAM,0BAA0B,GAAG;IACvC,iGAAiG;IACjG,QAAQ,CAAC,4BAA4B,CAAC,EAAE,MAAM,IAAI,CAAC;IACnD,mGAAmG;IACnG,QAAQ,CAAC,sBAAsB,CAAC,EAAE,2BAA2B,CAAC;CAC/D,CAAC;AAEF,4FAA4F;AAC5F,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,cAAc,EACd,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,CACnD,CAAC;AA+EF;;;;;;;GAOG;AACH,wBAAsB,6BAA6B,CACjD,KAAK,EAAE,gBAAgB,EACvB,cAAc,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,EACnD,sBAAsB,GAAE,2BAA2D,GAClF,OAAO,CAAC,UAAU,CAAC,CAqBrB;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,gBAAgB,EACvB,cAAc,EAAE,kBAAkB,EAClC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA0Df"}