@checkstack/healthcheck-http-backend 0.2.4 → 0.3.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/CHANGELOG.md +54 -0
- package/package.json +6 -6
- package/src/request-collector.test.ts +14 -10
- package/src/request-collector.ts +25 -27
- package/src/strategy.test.ts +17 -12
- package/src/strategy.ts +27 -31
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @checkstack/healthcheck-http-backend
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
8
|
+
|
|
9
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
10
|
+
|
|
11
|
+
**Key changes:**
|
|
12
|
+
|
|
13
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
14
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
15
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
16
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
17
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
18
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
19
|
+
|
|
20
|
+
**Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [3dd1914]
|
|
25
|
+
- @checkstack/backend-api@0.7.0
|
|
26
|
+
|
|
27
|
+
## 0.2.5
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
|
|
32
|
+
|
|
33
|
+
### Breaking Changes (Internal)
|
|
34
|
+
|
|
35
|
+
- Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
|
|
36
|
+
|
|
37
|
+
### New Features
|
|
38
|
+
|
|
39
|
+
- Added incremental aggregation utilities in `@checkstack/backend-api`:
|
|
40
|
+
- `mergeCounter()` - track occurrences
|
|
41
|
+
- `mergeAverage()` - track sum/count, compute avg
|
|
42
|
+
- `mergeRate()` - track success/total, compute %
|
|
43
|
+
- `mergeMinMax()` - track min/max values
|
|
44
|
+
- Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
|
|
45
|
+
|
|
46
|
+
### Improvements
|
|
47
|
+
|
|
48
|
+
- Enables O(1) storage overhead by maintaining incremental aggregation state
|
|
49
|
+
- Prepares for real-time hourly aggregation without batch accumulation
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [f676e11]
|
|
52
|
+
- Updated dependencies [48c2080]
|
|
53
|
+
- @checkstack/common@0.6.2
|
|
54
|
+
- @checkstack/backend-api@0.6.0
|
|
55
|
+
- @checkstack/healthcheck-common@0.8.2
|
|
56
|
+
|
|
3
57
|
## 0.2.4
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-http-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/backend-api": "0.5.
|
|
13
|
-
"@checkstack/healthcheck-common": "0.
|
|
12
|
+
"@checkstack/backend-api": "0.5.2",
|
|
13
|
+
"@checkstack/healthcheck-common": "0.8.1",
|
|
14
14
|
"jsonpath-plus": "^10.3.0",
|
|
15
|
-
"@checkstack/common": "0.6.
|
|
15
|
+
"@checkstack/common": "0.6.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/bun": "^1.0.0",
|
|
19
19
|
"drizzle-kit": "^0.31.8",
|
|
20
20
|
"typescript": "^5.0.0",
|
|
21
|
-
"@checkstack/tsconfig": "0.0.
|
|
22
|
-
"@checkstack/scripts": "0.1.
|
|
21
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
22
|
+
"@checkstack/scripts": "0.1.1"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -8,7 +8,7 @@ describe("RequestCollector", () => {
|
|
|
8
8
|
statusCode?: number;
|
|
9
9
|
statusText?: string;
|
|
10
10
|
body?: string;
|
|
11
|
-
} = {}
|
|
11
|
+
} = {},
|
|
12
12
|
): HttpTransportClient => ({
|
|
13
13
|
exec: mock(() =>
|
|
14
14
|
Promise.resolve({
|
|
@@ -16,7 +16,7 @@ describe("RequestCollector", () => {
|
|
|
16
16
|
statusText: response.statusText ?? "OK",
|
|
17
17
|
headers: {},
|
|
18
18
|
body: response.body ?? "",
|
|
19
|
-
})
|
|
19
|
+
}),
|
|
20
20
|
),
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -89,7 +89,7 @@ describe("RequestCollector", () => {
|
|
|
89
89
|
"Content-Type": "application/json",
|
|
90
90
|
Authorization: "Bearer token",
|
|
91
91
|
},
|
|
92
|
-
})
|
|
92
|
+
}),
|
|
93
93
|
);
|
|
94
94
|
});
|
|
95
95
|
|
|
@@ -111,12 +111,12 @@ describe("RequestCollector", () => {
|
|
|
111
111
|
expect(client.exec).toHaveBeenCalledWith(
|
|
112
112
|
expect.objectContaining({
|
|
113
113
|
body: '{"key":"value"}',
|
|
114
|
-
})
|
|
114
|
+
}),
|
|
115
115
|
);
|
|
116
116
|
});
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
describe("
|
|
119
|
+
describe("mergeResult", () => {
|
|
120
120
|
it("should calculate average response time", () => {
|
|
121
121
|
const collector = new RequestCollector();
|
|
122
122
|
const runs = [
|
|
@@ -152,10 +152,12 @@ describe("RequestCollector", () => {
|
|
|
152
152
|
},
|
|
153
153
|
];
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
// Merge runs incrementally
|
|
156
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
157
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
156
158
|
|
|
157
|
-
expect(aggregated.avgResponseTimeMs).toBe(75);
|
|
158
|
-
expect(aggregated.successRate).toBe(100);
|
|
159
|
+
expect(aggregated.avgResponseTimeMs.avg).toBe(75);
|
|
160
|
+
expect(aggregated.successRate.rate).toBe(100);
|
|
159
161
|
});
|
|
160
162
|
|
|
161
163
|
it("should calculate success rate correctly", () => {
|
|
@@ -193,9 +195,11 @@ describe("RequestCollector", () => {
|
|
|
193
195
|
},
|
|
194
196
|
];
|
|
195
197
|
|
|
196
|
-
|
|
198
|
+
// Merge runs incrementally
|
|
199
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
200
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
197
201
|
|
|
198
|
-
expect(aggregated.successRate).toBe(50);
|
|
202
|
+
expect(aggregated.successRate.rate).toBe(50);
|
|
199
203
|
});
|
|
200
204
|
});
|
|
201
205
|
|
package/src/request-collector.ts
CHANGED
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
type HealthCheckRunForAggregation,
|
|
6
6
|
type CollectorResult,
|
|
7
7
|
type CollectorStrategy,
|
|
8
|
+
mergeAverage,
|
|
9
|
+
mergeRate,
|
|
10
|
+
VersionedAggregated,
|
|
11
|
+
aggregatedAverage,
|
|
12
|
+
aggregatedRate,
|
|
13
|
+
type InferAggregatedResult,
|
|
8
14
|
} from "@checkstack/backend-api";
|
|
9
15
|
import {
|
|
10
16
|
healthResultNumber,
|
|
@@ -76,20 +82,24 @@ const requestResultSchema = healthResultSchema({
|
|
|
76
82
|
|
|
77
83
|
export type RequestResult = z.infer<typeof requestResultSchema>;
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
// Aggregated result fields definition
|
|
86
|
+
const requestAggregatedFields = {
|
|
87
|
+
avgResponseTimeMs: aggregatedAverage({
|
|
81
88
|
"x-chart-type": "line",
|
|
82
89
|
"x-chart-label": "Avg Response Time",
|
|
83
90
|
"x-chart-unit": "ms",
|
|
84
91
|
}),
|
|
85
|
-
successRate:
|
|
92
|
+
successRate: aggregatedRate({
|
|
86
93
|
"x-chart-type": "gauge",
|
|
87
94
|
"x-chart-label": "Success Rate",
|
|
88
95
|
"x-chart-unit": "%",
|
|
89
96
|
}),
|
|
90
|
-
}
|
|
97
|
+
};
|
|
91
98
|
|
|
92
|
-
|
|
99
|
+
// Type inferred automatically from field definitions
|
|
100
|
+
export type RequestAggregatedResult = InferAggregatedResult<
|
|
101
|
+
typeof requestAggregatedFields
|
|
102
|
+
>;
|
|
93
103
|
|
|
94
104
|
// ============================================================================
|
|
95
105
|
// REQUEST COLLECTOR
|
|
@@ -115,9 +125,9 @@ export class RequestCollector implements CollectorStrategy<
|
|
|
115
125
|
|
|
116
126
|
config = new Versioned({ version: 1, schema: requestConfigSchema });
|
|
117
127
|
result = new Versioned({ version: 1, schema: requestResultSchema });
|
|
118
|
-
aggregatedResult = new
|
|
128
|
+
aggregatedResult = new VersionedAggregated({
|
|
119
129
|
version: 1,
|
|
120
|
-
|
|
130
|
+
fields: requestAggregatedFields,
|
|
121
131
|
});
|
|
122
132
|
|
|
123
133
|
async execute({
|
|
@@ -162,28 +172,16 @@ export class RequestCollector implements CollectorStrategy<
|
|
|
162
172
|
};
|
|
163
173
|
}
|
|
164
174
|
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
mergeResult(
|
|
176
|
+
existing: RequestAggregatedResult | undefined,
|
|
177
|
+
newRun: HealthCheckRunForAggregation<RequestResult>,
|
|
167
178
|
): RequestAggregatedResult {
|
|
168
|
-
const times = runs
|
|
169
|
-
.map((r) => r.metadata?.responseTimeMs)
|
|
170
|
-
.filter((v): v is number => typeof v === "number");
|
|
171
|
-
|
|
172
|
-
const successes = runs
|
|
173
|
-
.map((r) => r.metadata?.success)
|
|
174
|
-
.filter((v): v is boolean => typeof v === "boolean");
|
|
175
|
-
|
|
176
|
-
const successCount = successes.filter(Boolean).length;
|
|
177
|
-
|
|
178
179
|
return {
|
|
179
|
-
avgResponseTimeMs:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
successRate:
|
|
184
|
-
successes.length > 0
|
|
185
|
-
? Math.round((successCount / successes.length) * 100)
|
|
186
|
-
: 0,
|
|
180
|
+
avgResponseTimeMs: mergeAverage(
|
|
181
|
+
existing?.avgResponseTimeMs,
|
|
182
|
+
newRun.metadata?.responseTimeMs,
|
|
183
|
+
),
|
|
184
|
+
successRate: mergeRate(existing?.successRate, newRun.metadata?.success),
|
|
187
185
|
};
|
|
188
186
|
}
|
|
189
187
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -31,7 +31,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
31
31
|
status: 200,
|
|
32
32
|
statusText: "OK",
|
|
33
33
|
headers: { "Content-Type": "application/json" },
|
|
34
|
-
})
|
|
34
|
+
}),
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
@@ -50,7 +50,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
50
50
|
|
|
51
51
|
it("should return 404 status for not found", async () => {
|
|
52
52
|
spyOn(globalThis, "fetch").mockResolvedValue(
|
|
53
|
-
new Response(null, { status: 404, statusText: "Not Found" })
|
|
53
|
+
new Response(null, { status: 404, statusText: "Not Found" }),
|
|
54
54
|
);
|
|
55
55
|
|
|
56
56
|
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
@@ -69,7 +69,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
69
69
|
let capturedHeaders: Record<string, string> | undefined;
|
|
70
70
|
spyOn(globalThis, "fetch").mockImplementation((async (
|
|
71
71
|
_url: RequestInfo | URL,
|
|
72
|
-
options?: RequestInit
|
|
72
|
+
options?: RequestInit,
|
|
73
73
|
) => {
|
|
74
74
|
capturedHeaders = options?.headers as Record<string, string>;
|
|
75
75
|
return new Response(null, { status: 200 });
|
|
@@ -99,7 +99,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
99
99
|
new Response(JSON.stringify(responseBody), {
|
|
100
100
|
status: 200,
|
|
101
101
|
headers: { "Content-Type": "application/json" },
|
|
102
|
-
})
|
|
102
|
+
}),
|
|
103
103
|
);
|
|
104
104
|
|
|
105
105
|
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
@@ -119,7 +119,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
119
119
|
new Response("Hello World", {
|
|
120
120
|
status: 200,
|
|
121
121
|
headers: { "Content-Type": "text/plain" },
|
|
122
|
-
})
|
|
122
|
+
}),
|
|
123
123
|
);
|
|
124
124
|
|
|
125
125
|
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
|
@@ -138,7 +138,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
138
138
|
let capturedBody: string | undefined;
|
|
139
139
|
spyOn(globalThis, "fetch").mockImplementation((async (
|
|
140
140
|
_url: RequestInfo | URL,
|
|
141
|
-
options?: RequestInit
|
|
141
|
+
options?: RequestInit,
|
|
142
142
|
) => {
|
|
143
143
|
capturedBody = options?.body as string;
|
|
144
144
|
return new Response(null, { status: 201 });
|
|
@@ -161,7 +161,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
161
161
|
let capturedMethod: string | undefined;
|
|
162
162
|
spyOn(globalThis, "fetch").mockImplementation((async (
|
|
163
163
|
_url: RequestInfo | URL,
|
|
164
|
-
options?: RequestInit
|
|
164
|
+
options?: RequestInit,
|
|
165
165
|
) => {
|
|
166
166
|
capturedMethod = options?.method;
|
|
167
167
|
return new Response(null, { status: 200 });
|
|
@@ -180,7 +180,7 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
180
180
|
});
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
describe("
|
|
183
|
+
describe("mergeResult", () => {
|
|
184
184
|
it("should count errors correctly", () => {
|
|
185
185
|
const runs = [
|
|
186
186
|
{
|
|
@@ -213,9 +213,12 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
213
213
|
},
|
|
214
214
|
];
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
// Merge runs incrementally
|
|
217
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
218
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
219
|
+
aggregated = strategy.mergeResult(aggregated, runs[2]);
|
|
217
220
|
|
|
218
|
-
expect(aggregated.errorCount).toBe(2);
|
|
221
|
+
expect(aggregated.errorCount.count).toBe(2);
|
|
219
222
|
});
|
|
220
223
|
|
|
221
224
|
it("should return zero errors when all runs succeed", () => {
|
|
@@ -238,9 +241,11 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
238
241
|
},
|
|
239
242
|
];
|
|
240
243
|
|
|
241
|
-
|
|
244
|
+
// Merge runs incrementally
|
|
245
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
246
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
242
247
|
|
|
243
|
-
expect(aggregated.errorCount).toBe(0);
|
|
248
|
+
expect(aggregated.errorCount.count).toBe(0);
|
|
244
249
|
});
|
|
245
250
|
});
|
|
246
251
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -2,11 +2,14 @@ import {
|
|
|
2
2
|
HealthCheckStrategy,
|
|
3
3
|
HealthCheckRunForAggregation,
|
|
4
4
|
Versioned,
|
|
5
|
+
VersionedAggregated,
|
|
6
|
+
aggregatedCounter,
|
|
7
|
+
mergeCounter,
|
|
5
8
|
z,
|
|
9
|
+
type InferAggregatedResult,
|
|
6
10
|
type ConnectedClient,
|
|
7
11
|
} from "@checkstack/backend-api";
|
|
8
12
|
import {
|
|
9
|
-
healthResultNumber,
|
|
10
13
|
healthResultString,
|
|
11
14
|
healthResultSchema,
|
|
12
15
|
} from "@checkstack/healthcheck-common";
|
|
@@ -57,29 +60,26 @@ const httpResultMetadataSchema = healthResultSchema({
|
|
|
57
60
|
|
|
58
61
|
type HttpResultMetadata = z.infer<typeof httpResultMetadataSchema>;
|
|
59
62
|
|
|
60
|
-
/** Aggregated
|
|
61
|
-
const
|
|
62
|
-
errorCount:
|
|
63
|
+
/** Aggregated field definitions for bucket merging */
|
|
64
|
+
const httpAggregatedFields = {
|
|
65
|
+
errorCount: aggregatedCounter({
|
|
63
66
|
"x-chart-type": "counter",
|
|
64
67
|
"x-chart-label": "Errors",
|
|
65
68
|
}),
|
|
66
|
-
}
|
|
69
|
+
};
|
|
67
70
|
|
|
68
|
-
type
|
|
71
|
+
type HttpAggregatedResult = InferAggregatedResult<typeof httpAggregatedFields>;
|
|
69
72
|
|
|
70
73
|
// ============================================================================
|
|
71
74
|
// STRATEGY
|
|
72
75
|
// ============================================================================
|
|
73
76
|
|
|
74
|
-
export class HttpHealthCheckStrategy
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
HttpAggregatedMetadata
|
|
81
|
-
>
|
|
82
|
-
{
|
|
77
|
+
export class HttpHealthCheckStrategy implements HealthCheckStrategy<
|
|
78
|
+
HttpHealthCheckConfig,
|
|
79
|
+
HttpTransportClient,
|
|
80
|
+
HttpResultMetadata,
|
|
81
|
+
typeof httpAggregatedFields
|
|
82
|
+
> {
|
|
83
83
|
id = "http";
|
|
84
84
|
displayName = "HTTP/HTTPS Health Check";
|
|
85
85
|
description = "HTTP endpoint health monitoring";
|
|
@@ -114,23 +114,19 @@ export class HttpHealthCheckStrategy
|
|
|
114
114
|
schema: httpResultMetadataSchema,
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
aggregatedResult
|
|
117
|
+
aggregatedResult = new VersionedAggregated({
|
|
118
118
|
version: 1,
|
|
119
|
-
|
|
119
|
+
fields: httpAggregatedFields,
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { errorCount };
|
|
122
|
+
mergeResult(
|
|
123
|
+
existing: HttpAggregatedResult | undefined,
|
|
124
|
+
newRun: HealthCheckRunForAggregation<HttpResultMetadata>,
|
|
125
|
+
): HttpAggregatedResult {
|
|
126
|
+
const hasError = !!newRun.metadata?.error;
|
|
127
|
+
return {
|
|
128
|
+
errorCount: mergeCounter(existing?.errorCount, hasError),
|
|
129
|
+
};
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
/**
|
|
@@ -138,7 +134,7 @@ export class HttpHealthCheckStrategy
|
|
|
138
134
|
* All request parameters come from the collector (RequestCollector).
|
|
139
135
|
*/
|
|
140
136
|
async createClient(
|
|
141
|
-
config: HttpHealthCheckConfig
|
|
137
|
+
config: HttpHealthCheckConfig,
|
|
142
138
|
): Promise<ConnectedClient<HttpTransportClient>> {
|
|
143
139
|
const validatedConfig = this.config.validate(config);
|
|
144
140
|
|
|
@@ -147,7 +143,7 @@ export class HttpHealthCheckStrategy
|
|
|
147
143
|
const controller = new AbortController();
|
|
148
144
|
const timeoutId = setTimeout(
|
|
149
145
|
() => controller.abort(),
|
|
150
|
-
request.timeout ?? validatedConfig.timeout
|
|
146
|
+
request.timeout ?? validatedConfig.timeout,
|
|
151
147
|
);
|
|
152
148
|
|
|
153
149
|
try {
|