qlara 0.1.2 → 0.1.3
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/dist/aws.cjs +25 -4
- package/dist/aws.d.cts +10 -0
- package/dist/aws.d.ts +10 -0
- package/dist/aws.js +26 -4
- package/dist/cli.js +26 -4
- package/package.json +1 -1
- package/src/provider/aws/edge-handler.ts +2 -1
- package/src/provider/aws/renderer.ts +25 -19
package/dist/aws.cjs
CHANGED
|
@@ -520,14 +520,15 @@ async function bundleEdgeHandler(config) {
|
|
|
520
520
|
define: {
|
|
521
521
|
__QLARA_BUCKET_NAME__: JSON.stringify(config.bucketName),
|
|
522
522
|
__QLARA_RENDERER_ARN__: JSON.stringify(config.rendererArn),
|
|
523
|
-
__QLARA_REGION__: JSON.stringify(config.region)
|
|
523
|
+
__QLARA_REGION__: JSON.stringify(config.region),
|
|
524
|
+
__QLARA_CACHE_TTL__: String(config.cacheTtl)
|
|
524
525
|
},
|
|
525
526
|
// Bundle everything — Lambda@Edge must be self-contained
|
|
526
527
|
external: []
|
|
527
528
|
});
|
|
528
529
|
return createZip(outfile, "edge-handler.js");
|
|
529
530
|
}
|
|
530
|
-
async function bundleRenderer(routeFile) {
|
|
531
|
+
async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
531
532
|
(0, import_node_fs2.mkdirSync)(BUNDLE_DIR, { recursive: true });
|
|
532
533
|
const outfile = (0, import_node_path2.join)(BUNDLE_DIR, "renderer.js");
|
|
533
534
|
const alias = {};
|
|
@@ -548,6 +549,9 @@ async function bundleRenderer(routeFile) {
|
|
|
548
549
|
outfile,
|
|
549
550
|
minify: true,
|
|
550
551
|
alias,
|
|
552
|
+
define: {
|
|
553
|
+
__QLARA_CACHE_TTL__: String(cacheTtl)
|
|
554
|
+
},
|
|
551
555
|
external: []
|
|
552
556
|
});
|
|
553
557
|
return createZip(outfile, "renderer.js");
|
|
@@ -764,6 +768,7 @@ function byoiResources(config) {
|
|
|
764
768
|
function aws(awsConfig = {}) {
|
|
765
769
|
const region = "us-east-1";
|
|
766
770
|
const byoi = isByoi(awsConfig);
|
|
771
|
+
const cacheTtl = awsConfig.cacheTtl ?? 3600;
|
|
767
772
|
const stackName = byoi ? "" : awsConfig.stackName || STACK_NAME_PREFIX;
|
|
768
773
|
return {
|
|
769
774
|
name: "aws",
|
|
@@ -845,7 +850,8 @@ function aws(awsConfig = {}) {
|
|
|
845
850
|
const edgeZip = await bundleEdgeHandler({
|
|
846
851
|
bucketName: res.bucketName,
|
|
847
852
|
rendererArn: res.rendererFunctionArn,
|
|
848
|
-
region: res.region
|
|
853
|
+
region: res.region,
|
|
854
|
+
cacheTtl
|
|
849
855
|
});
|
|
850
856
|
const lambda = new import_client_lambda.LambdaClient({ region: res.region });
|
|
851
857
|
console.log("[qlara/aws] Waiting for edge handler to be ready...");
|
|
@@ -922,7 +928,7 @@ function aws(awsConfig = {}) {
|
|
|
922
928
|
const cf = new import_client_cloudfront.CloudFrontClient({ region: res.region });
|
|
923
929
|
await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
|
|
924
930
|
console.log("[qlara/aws] Bundling renderer...");
|
|
925
|
-
const rendererZip = await bundleRenderer(config.routeFile);
|
|
931
|
+
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
|
|
926
932
|
await (0, import_client_lambda.waitUntilFunctionUpdatedV2)(
|
|
927
933
|
{ client: lambda, maxWaitTime: 120 },
|
|
928
934
|
{ FunctionName: res.rendererFunctionArn }
|
|
@@ -997,6 +1003,21 @@ function aws(awsConfig = {}) {
|
|
|
997
1003
|
`[qlara/aws] Could not update renderer permissions: ${err.message}`
|
|
998
1004
|
);
|
|
999
1005
|
}
|
|
1006
|
+
console.log("[qlara/aws] Warming up renderer...");
|
|
1007
|
+
try {
|
|
1008
|
+
await lambda.send(
|
|
1009
|
+
new import_client_lambda.InvokeCommand({
|
|
1010
|
+
FunctionName: res.rendererFunctionArn,
|
|
1011
|
+
InvocationType: "RequestResponse",
|
|
1012
|
+
Payload: JSON.stringify({ warmup: true })
|
|
1013
|
+
})
|
|
1014
|
+
);
|
|
1015
|
+
console.log("[qlara/aws] Renderer warmed up");
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
console.warn(
|
|
1018
|
+
`[qlara/aws] Renderer warm-up failed (non-critical): ${err.message}`
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1000
1021
|
console.log("[qlara/aws] Invalidating CloudFront cache...");
|
|
1001
1022
|
await cf.send(
|
|
1002
1023
|
new import_client_cloudfront.CreateInvalidationCommand({
|
package/dist/aws.d.cts
CHANGED
|
@@ -3,6 +3,16 @@ import { P as ProviderResources, Q as QlaraProvider } from './types-gl2xFqEX.cjs
|
|
|
3
3
|
interface AwsConfig {
|
|
4
4
|
stackName?: string;
|
|
5
5
|
bucketName?: string;
|
|
6
|
+
/**
|
|
7
|
+
* How long (in seconds) CloudFront should cache dynamically rendered pages.
|
|
8
|
+
* This sets the `s-maxage` value on the Cache-Control header.
|
|
9
|
+
*
|
|
10
|
+
* - Browsers always revalidate with CloudFront (`max-age=0`)
|
|
11
|
+
* - CloudFront serves from edge cache for this duration
|
|
12
|
+
*
|
|
13
|
+
* @default 3600 (1 hour)
|
|
14
|
+
*/
|
|
15
|
+
cacheTtl?: number;
|
|
6
16
|
distributionId?: string;
|
|
7
17
|
distributionDomain?: string;
|
|
8
18
|
edgeFunctionArn?: string;
|
package/dist/aws.d.ts
CHANGED
|
@@ -3,6 +3,16 @@ import { P as ProviderResources, Q as QlaraProvider } from './types-gl2xFqEX.js'
|
|
|
3
3
|
interface AwsConfig {
|
|
4
4
|
stackName?: string;
|
|
5
5
|
bucketName?: string;
|
|
6
|
+
/**
|
|
7
|
+
* How long (in seconds) CloudFront should cache dynamically rendered pages.
|
|
8
|
+
* This sets the `s-maxage` value on the Cache-Control header.
|
|
9
|
+
*
|
|
10
|
+
* - Browsers always revalidate with CloudFront (`max-age=0`)
|
|
11
|
+
* - CloudFront serves from edge cache for this duration
|
|
12
|
+
*
|
|
13
|
+
* @default 3600 (1 hour)
|
|
14
|
+
*/
|
|
15
|
+
cacheTtl?: number;
|
|
6
16
|
distributionId?: string;
|
|
7
17
|
distributionDomain?: string;
|
|
8
18
|
edgeFunctionArn?: string;
|
package/dist/aws.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
UpdateFunctionConfigurationCommand,
|
|
16
16
|
PublishVersionCommand,
|
|
17
17
|
AddPermissionCommand,
|
|
18
|
+
InvokeCommand,
|
|
18
19
|
waitUntilFunctionUpdatedV2
|
|
19
20
|
} from "@aws-sdk/client-lambda";
|
|
20
21
|
import {
|
|
@@ -515,14 +516,15 @@ async function bundleEdgeHandler(config) {
|
|
|
515
516
|
define: {
|
|
516
517
|
__QLARA_BUCKET_NAME__: JSON.stringify(config.bucketName),
|
|
517
518
|
__QLARA_RENDERER_ARN__: JSON.stringify(config.rendererArn),
|
|
518
|
-
__QLARA_REGION__: JSON.stringify(config.region)
|
|
519
|
+
__QLARA_REGION__: JSON.stringify(config.region),
|
|
520
|
+
__QLARA_CACHE_TTL__: String(config.cacheTtl)
|
|
519
521
|
},
|
|
520
522
|
// Bundle everything — Lambda@Edge must be self-contained
|
|
521
523
|
external: []
|
|
522
524
|
});
|
|
523
525
|
return createZip(outfile, "edge-handler.js");
|
|
524
526
|
}
|
|
525
|
-
async function bundleRenderer(routeFile) {
|
|
527
|
+
async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
526
528
|
mkdirSync(BUNDLE_DIR, { recursive: true });
|
|
527
529
|
const outfile = join2(BUNDLE_DIR, "renderer.js");
|
|
528
530
|
const alias = {};
|
|
@@ -543,6 +545,9 @@ async function bundleRenderer(routeFile) {
|
|
|
543
545
|
outfile,
|
|
544
546
|
minify: true,
|
|
545
547
|
alias,
|
|
548
|
+
define: {
|
|
549
|
+
__QLARA_CACHE_TTL__: String(cacheTtl)
|
|
550
|
+
},
|
|
546
551
|
external: []
|
|
547
552
|
});
|
|
548
553
|
return createZip(outfile, "renderer.js");
|
|
@@ -759,6 +764,7 @@ function byoiResources(config) {
|
|
|
759
764
|
function aws(awsConfig = {}) {
|
|
760
765
|
const region = "us-east-1";
|
|
761
766
|
const byoi = isByoi(awsConfig);
|
|
767
|
+
const cacheTtl = awsConfig.cacheTtl ?? 3600;
|
|
762
768
|
const stackName = byoi ? "" : awsConfig.stackName || STACK_NAME_PREFIX;
|
|
763
769
|
return {
|
|
764
770
|
name: "aws",
|
|
@@ -840,7 +846,8 @@ function aws(awsConfig = {}) {
|
|
|
840
846
|
const edgeZip = await bundleEdgeHandler({
|
|
841
847
|
bucketName: res.bucketName,
|
|
842
848
|
rendererArn: res.rendererFunctionArn,
|
|
843
|
-
region: res.region
|
|
849
|
+
region: res.region,
|
|
850
|
+
cacheTtl
|
|
844
851
|
});
|
|
845
852
|
const lambda = new LambdaClient({ region: res.region });
|
|
846
853
|
console.log("[qlara/aws] Waiting for edge handler to be ready...");
|
|
@@ -917,7 +924,7 @@ function aws(awsConfig = {}) {
|
|
|
917
924
|
const cf = new CloudFrontClient({ region: res.region });
|
|
918
925
|
await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
|
|
919
926
|
console.log("[qlara/aws] Bundling renderer...");
|
|
920
|
-
const rendererZip = await bundleRenderer(config.routeFile);
|
|
927
|
+
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
|
|
921
928
|
await waitUntilFunctionUpdatedV2(
|
|
922
929
|
{ client: lambda, maxWaitTime: 120 },
|
|
923
930
|
{ FunctionName: res.rendererFunctionArn }
|
|
@@ -992,6 +999,21 @@ function aws(awsConfig = {}) {
|
|
|
992
999
|
`[qlara/aws] Could not update renderer permissions: ${err.message}`
|
|
993
1000
|
);
|
|
994
1001
|
}
|
|
1002
|
+
console.log("[qlara/aws] Warming up renderer...");
|
|
1003
|
+
try {
|
|
1004
|
+
await lambda.send(
|
|
1005
|
+
new InvokeCommand({
|
|
1006
|
+
FunctionName: res.rendererFunctionArn,
|
|
1007
|
+
InvocationType: "RequestResponse",
|
|
1008
|
+
Payload: JSON.stringify({ warmup: true })
|
|
1009
|
+
})
|
|
1010
|
+
);
|
|
1011
|
+
console.log("[qlara/aws] Renderer warmed up");
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
console.warn(
|
|
1014
|
+
`[qlara/aws] Renderer warm-up failed (non-critical): ${err.message}`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
995
1017
|
console.log("[qlara/aws] Invalidating CloudFront cache...");
|
|
996
1018
|
await cf.send(
|
|
997
1019
|
new CreateInvalidationCommand({
|
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
UpdateFunctionConfigurationCommand,
|
|
27
27
|
PublishVersionCommand,
|
|
28
28
|
AddPermissionCommand,
|
|
29
|
+
InvokeCommand,
|
|
29
30
|
waitUntilFunctionUpdatedV2
|
|
30
31
|
} from "@aws-sdk/client-lambda";
|
|
31
32
|
import {
|
|
@@ -526,14 +527,15 @@ async function bundleEdgeHandler(config) {
|
|
|
526
527
|
define: {
|
|
527
528
|
__QLARA_BUCKET_NAME__: JSON.stringify(config.bucketName),
|
|
528
529
|
__QLARA_RENDERER_ARN__: JSON.stringify(config.rendererArn),
|
|
529
|
-
__QLARA_REGION__: JSON.stringify(config.region)
|
|
530
|
+
__QLARA_REGION__: JSON.stringify(config.region),
|
|
531
|
+
__QLARA_CACHE_TTL__: String(config.cacheTtl)
|
|
530
532
|
},
|
|
531
533
|
// Bundle everything — Lambda@Edge must be self-contained
|
|
532
534
|
external: []
|
|
533
535
|
});
|
|
534
536
|
return createZip(outfile, "edge-handler.js");
|
|
535
537
|
}
|
|
536
|
-
async function bundleRenderer(routeFile) {
|
|
538
|
+
async function bundleRenderer(routeFile, cacheTtl = 3600) {
|
|
537
539
|
mkdirSync(BUNDLE_DIR, { recursive: true });
|
|
538
540
|
const outfile = join2(BUNDLE_DIR, "renderer.js");
|
|
539
541
|
const alias = {};
|
|
@@ -554,6 +556,9 @@ async function bundleRenderer(routeFile) {
|
|
|
554
556
|
outfile,
|
|
555
557
|
minify: true,
|
|
556
558
|
alias,
|
|
559
|
+
define: {
|
|
560
|
+
__QLARA_CACHE_TTL__: String(cacheTtl)
|
|
561
|
+
},
|
|
557
562
|
external: []
|
|
558
563
|
});
|
|
559
564
|
return createZip(outfile, "renderer.js");
|
|
@@ -767,6 +772,7 @@ function byoiResources(config) {
|
|
|
767
772
|
function aws(awsConfig = {}) {
|
|
768
773
|
const region = "us-east-1";
|
|
769
774
|
const byoi = isByoi(awsConfig);
|
|
775
|
+
const cacheTtl = awsConfig.cacheTtl ?? 3600;
|
|
770
776
|
const stackName = byoi ? "" : awsConfig.stackName || STACK_NAME_PREFIX;
|
|
771
777
|
return {
|
|
772
778
|
name: "aws",
|
|
@@ -848,7 +854,8 @@ function aws(awsConfig = {}) {
|
|
|
848
854
|
const edgeZip = await bundleEdgeHandler({
|
|
849
855
|
bucketName: res.bucketName,
|
|
850
856
|
rendererArn: res.rendererFunctionArn,
|
|
851
|
-
region: res.region
|
|
857
|
+
region: res.region,
|
|
858
|
+
cacheTtl
|
|
852
859
|
});
|
|
853
860
|
const lambda = new LambdaClient({ region: res.region });
|
|
854
861
|
console.log("[qlara/aws] Waiting for edge handler to be ready...");
|
|
@@ -925,7 +932,7 @@ function aws(awsConfig = {}) {
|
|
|
925
932
|
const cf = new CloudFrontClient({ region: res.region });
|
|
926
933
|
await updateCloudFrontEdgeVersion(cf, res.distributionId, newVersionArn);
|
|
927
934
|
console.log("[qlara/aws] Bundling renderer...");
|
|
928
|
-
const rendererZip = await bundleRenderer(config.routeFile);
|
|
935
|
+
const rendererZip = await bundleRenderer(config.routeFile, cacheTtl);
|
|
929
936
|
await waitUntilFunctionUpdatedV2(
|
|
930
937
|
{ client: lambda, maxWaitTime: 120 },
|
|
931
938
|
{ FunctionName: res.rendererFunctionArn }
|
|
@@ -1000,6 +1007,21 @@ function aws(awsConfig = {}) {
|
|
|
1000
1007
|
`[qlara/aws] Could not update renderer permissions: ${err.message}`
|
|
1001
1008
|
);
|
|
1002
1009
|
}
|
|
1010
|
+
console.log("[qlara/aws] Warming up renderer...");
|
|
1011
|
+
try {
|
|
1012
|
+
await lambda.send(
|
|
1013
|
+
new InvokeCommand({
|
|
1014
|
+
FunctionName: res.rendererFunctionArn,
|
|
1015
|
+
InvocationType: "RequestResponse",
|
|
1016
|
+
Payload: JSON.stringify({ warmup: true })
|
|
1017
|
+
})
|
|
1018
|
+
);
|
|
1019
|
+
console.log("[qlara/aws] Renderer warmed up");
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
console.warn(
|
|
1022
|
+
`[qlara/aws] Renderer warm-up failed (non-critical): ${err.message}`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1003
1025
|
console.log("[qlara/aws] Invalidating CloudFront cache...");
|
|
1004
1026
|
await cf.send(
|
|
1005
1027
|
new CreateInvalidationCommand({
|
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
|
|
|
24
24
|
declare const __QLARA_BUCKET_NAME__: string;
|
|
25
25
|
declare const __QLARA_RENDERER_ARN__: string;
|
|
26
26
|
declare const __QLARA_REGION__: string;
|
|
27
|
+
declare const __QLARA_CACHE_TTL__: number;
|
|
27
28
|
// ── Types (inlined to keep bundle self-contained) ────────────────
|
|
28
29
|
|
|
29
30
|
interface ManifestRoute {
|
|
@@ -238,7 +239,7 @@ function buildHtmlResponse(
|
|
|
238
239
|
{ key: 'Content-Type', value: 'text/html; charset=utf-8' },
|
|
239
240
|
],
|
|
240
241
|
'cache-control': [
|
|
241
|
-
{ key: 'Cache-Control', value:
|
|
242
|
+
{ key: 'Cache-Control', value: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60` },
|
|
242
243
|
],
|
|
243
244
|
};
|
|
244
245
|
|
|
@@ -52,6 +52,9 @@ import type {
|
|
|
52
52
|
// The routes module is resolved by esbuild at deploy time.
|
|
53
53
|
// esbuild's `alias` option maps this import to the developer's route file.
|
|
54
54
|
// At bundle time: '__qlara_routes__' → './qlara.routes.ts' (or wherever the dev put it)
|
|
55
|
+
// Injected at bundle time by esbuild define
|
|
56
|
+
declare const __QLARA_CACHE_TTL__: number;
|
|
57
|
+
|
|
55
58
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
56
59
|
// @ts-ignore — resolved at bundle time by esbuild alias
|
|
57
60
|
import routes from '__qlara_routes__';
|
|
@@ -752,20 +755,30 @@ function metadataToRscEntries(metadata: QlaraMetadata): string {
|
|
|
752
755
|
return entries.join('');
|
|
753
756
|
}
|
|
754
757
|
|
|
755
|
-
export async function handler(event: RendererEvent): Promise<RendererResult> {
|
|
758
|
+
export async function handler(event: RendererEvent & { warmup?: boolean }): Promise<RendererResult> {
|
|
759
|
+
// Warmup invocation — just initialize the runtime and return
|
|
760
|
+
if (event.warmup) {
|
|
761
|
+
return { statusCode: 200, body: 'warm' };
|
|
762
|
+
}
|
|
763
|
+
|
|
756
764
|
const { uri, bucket, routePattern, params } = event;
|
|
757
765
|
const region = process.env.AWS_REGION || 'us-east-1';
|
|
758
766
|
|
|
759
767
|
const s3 = new S3Client({ region });
|
|
760
768
|
|
|
761
769
|
try {
|
|
762
|
-
// 0. Check if
|
|
770
|
+
// 0. Check if already rendered + read fallback in parallel
|
|
763
771
|
const s3Key = deriveS3Key(uri);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
)
|
|
768
|
-
|
|
772
|
+
const fallbackKey = deriveFallbackKey(uri);
|
|
773
|
+
|
|
774
|
+
const [existingResult, fallbackResult] = await Promise.allSettled([
|
|
775
|
+
s3.send(new GetObjectCommand({ Bucket: bucket, Key: s3Key })),
|
|
776
|
+
s3.send(new GetObjectCommand({ Bucket: bucket, Key: fallbackKey })),
|
|
777
|
+
]);
|
|
778
|
+
|
|
779
|
+
// If page already exists, return it (guards against duplicate concurrent requests)
|
|
780
|
+
if (existingResult.status === 'fulfilled') {
|
|
781
|
+
const existingHtml = await existingResult.value.Body?.transformToString('utf-8');
|
|
769
782
|
if (existingHtml) {
|
|
770
783
|
return {
|
|
771
784
|
statusCode: 200,
|
|
@@ -773,26 +786,20 @@ export async function handler(event: RendererEvent): Promise<RendererResult> {
|
|
|
773
786
|
html: existingHtml,
|
|
774
787
|
};
|
|
775
788
|
}
|
|
776
|
-
} catch {
|
|
777
|
-
// Page doesn't exist yet — continue with rendering
|
|
778
789
|
}
|
|
779
790
|
|
|
780
|
-
//
|
|
781
|
-
const fallbackKey = deriveFallbackKey(uri);
|
|
791
|
+
// Extract fallback HTML
|
|
782
792
|
let fallbackHtml: string;
|
|
783
793
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
new GetObjectCommand({ Bucket: bucket, Key: fallbackKey })
|
|
787
|
-
);
|
|
788
|
-
fallbackHtml = (await response.Body?.transformToString('utf-8')) || '';
|
|
794
|
+
if (fallbackResult.status === 'fulfilled') {
|
|
795
|
+
fallbackHtml = (await fallbackResult.value.Body?.transformToString('utf-8')) || '';
|
|
789
796
|
if (!fallbackHtml) {
|
|
790
797
|
return {
|
|
791
798
|
statusCode: 500,
|
|
792
799
|
body: JSON.stringify({ error: `Empty fallback at ${fallbackKey}` }),
|
|
793
800
|
};
|
|
794
801
|
}
|
|
795
|
-
}
|
|
802
|
+
} else {
|
|
796
803
|
return {
|
|
797
804
|
statusCode: 500,
|
|
798
805
|
body: JSON.stringify({ error: `Fallback not found: ${fallbackKey}` }),
|
|
@@ -824,8 +831,7 @@ export async function handler(event: RendererEvent): Promise<RendererResult> {
|
|
|
824
831
|
Key: s3Key,
|
|
825
832
|
Body: html,
|
|
826
833
|
ContentType: 'text/html; charset=utf-8',
|
|
827
|
-
CacheControl:
|
|
828
|
-
'public, max-age=0, s-maxage=31536000, stale-while-revalidate=86400',
|
|
834
|
+
CacheControl: `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60`,
|
|
829
835
|
})
|
|
830
836
|
);
|
|
831
837
|
|