immortal-js 1.0.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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +577 -0
  3. package/docs/CHANGELOG.md +21 -0
  4. package/docs/CONTRIBUTING.md +41 -0
  5. package/docs/api/chaos.md +179 -0
  6. package/docs/api/dashboard.md +109 -0
  7. package/docs/api/isolation.md +191 -0
  8. package/docs/api/lifecycle.md +187 -0
  9. package/docs/api/monitoring.md +313 -0
  10. package/docs/api/plugins.md +217 -0
  11. package/docs/api/recovery.md +267 -0
  12. package/docs/api/safe-zone.md +236 -0
  13. package/docs/api/supervision.md +285 -0
  14. package/docs/guides/configuration.md +168 -0
  15. package/docs/guides/express.md +171 -0
  16. package/docs/guides/fastify.md +188 -0
  17. package/docs/guides/koa.md +102 -0
  18. package/docs/guides/nestjs.md +182 -0
  19. package/docs/guides/testing.md +91 -0
  20. package/examples/express-basic/index.ts +462 -0
  21. package/examples/express-basic/package.json +21 -0
  22. package/examples/express-basic/tsconfig.json +24 -0
  23. package/examples/fastify-microservice/index.ts +342 -0
  24. package/examples/fastify-microservice/package.json +22 -0
  25. package/examples/fastify-microservice/tsconfig.json +24 -0
  26. package/examples/invoice-service/data/invoices.db +0 -0
  27. package/examples/invoice-service/data/invoices.db-shm +0 -0
  28. package/examples/invoice-service/data/invoices.db-wal +0 -0
  29. package/examples/invoice-service/package.json +25 -0
  30. package/examples/invoice-service/public/index.html +5025 -0
  31. package/examples/invoice-service/src/db.ts +608 -0
  32. package/examples/invoice-service/src/pdf.ts +358 -0
  33. package/examples/invoice-service/src/server.ts +527 -0
  34. package/examples/invoice-service/src/store.ts +159 -0
  35. package/examples/invoice-service/src/types.ts +193 -0
  36. package/examples/invoice-service/tsconfig.json +23 -0
  37. package/examples/nestjs-enterprise/app.module.ts +561 -0
  38. package/examples/nestjs-enterprise/main.ts +67 -0
  39. package/examples/nestjs-enterprise/package.json +26 -0
  40. package/examples/nestjs-enterprise/tsconfig.json +27 -0
  41. package/immortal-js-1.0.0.tgz +0 -0
  42. package/package.json +33 -0
  43. package/packages/adapter-express/package.json +34 -0
  44. package/packages/adapter-express/src/index.ts +349 -0
  45. package/packages/adapter-express/tsconfig.json +14 -0
  46. package/packages/adapter-fastify/package.json +56 -0
  47. package/packages/adapter-fastify/src/plugin.ts +226 -0
  48. package/packages/adapter-fastify/tsconfig.json +14 -0
  49. package/packages/adapter-koa/package.json +55 -0
  50. package/packages/adapter-koa/src/index.ts +207 -0
  51. package/packages/adapter-koa/tsconfig.json +14 -0
  52. package/packages/adapter-nestjs/package.json +61 -0
  53. package/packages/adapter-nestjs/src/immortal.module.ts +313 -0
  54. package/packages/adapter-nestjs/src/index.ts +14 -0
  55. package/packages/adapter-nestjs/tsconfig.json +16 -0
  56. package/packages/core/package.json +56 -0
  57. package/packages/core/src/chaos/ChaosEngine.ts +249 -0
  58. package/packages/core/src/config/defaults.ts +200 -0
  59. package/packages/core/src/config/schema.ts +199 -0
  60. package/packages/core/src/event-bus.ts +168 -0
  61. package/packages/core/src/index.ts +164 -0
  62. package/packages/core/src/isolation/BulkheadPool.ts +279 -0
  63. package/packages/core/src/isolation/WorkerSandbox.ts +306 -0
  64. package/packages/core/src/isolation/index.ts +8 -0
  65. package/packages/core/src/lifecycle/GracefulShutdown.ts +161 -0
  66. package/packages/core/src/logger.ts +104 -0
  67. package/packages/core/src/monitoring/DiagnosticsChannel.ts +248 -0
  68. package/packages/core/src/monitoring/HealthMonitor.ts +191 -0
  69. package/packages/core/src/monitoring/MemoryLeakGuard.ts +340 -0
  70. package/packages/core/src/monitoring/MetricsCollector.ts +219 -0
  71. package/packages/core/src/monitoring/index.ts +10 -0
  72. package/packages/core/src/plugins/BuiltinPlugins.ts +269 -0
  73. package/packages/core/src/recovery/CircuitBreaker.ts +334 -0
  74. package/packages/core/src/recovery/FallbackCache.ts +328 -0
  75. package/packages/core/src/recovery/RetryEngine.ts +225 -0
  76. package/packages/core/src/recovery/Timeout.ts +97 -0
  77. package/packages/core/src/recovery/index.ts +11 -0
  78. package/packages/core/src/runtime.ts +242 -0
  79. package/packages/core/src/safe-zone/AsyncBoundary.ts +114 -0
  80. package/packages/core/src/safe-zone/ErrorTrap.ts +347 -0
  81. package/packages/core/src/safe-zone/SafeWrapper.ts +317 -0
  82. package/packages/core/src/safe-zone/index.ts +23 -0
  83. package/packages/core/src/supervision/ClusterManager.ts +243 -0
  84. package/packages/core/src/supervision/RestartStrategy.ts +68 -0
  85. package/packages/core/src/supervision/Supervisor.ts +311 -0
  86. package/packages/core/src/supervision/index.ts +11 -0
  87. package/packages/core/src/types.ts +470 -0
  88. package/packages/core/test/bulkhead.test.ts +310 -0
  89. package/packages/core/test/circuit-breaker.test.ts +153 -0
  90. package/packages/core/test/memory-guard.test.ts +213 -0
  91. package/packages/core/test/retry.test.ts +110 -0
  92. package/packages/core/test/safe-zone.test.ts +271 -0
  93. package/packages/core/test/supervisor.test.ts +310 -0
  94. package/packages/core/tsconfig.json +13 -0
  95. package/packages/dashboard/package.json +56 -0
  96. package/packages/dashboard/server/DashboardServer.ts +454 -0
  97. package/packages/dashboard/tsconfig.json +14 -0
  98. package/tsconfig.json +25 -0
@@ -0,0 +1,462 @@
1
+ /**
2
+ * @file examples/express-basic/index.ts
3
+ * @description Immortal.js — Express Resilience Example (Ultra Pro Edition)
4
+ *
5
+ * Demonstrates:
6
+ * - CircuitBreaker — wraps unreliable external APIs
7
+ * - BulkheadPool — per-service concurrency isolation
8
+ * - FallbackCache — serve stale data when backends fail
9
+ * - GracefulShutdown — drain connections before process exit
10
+ * - withRetry — exponential back-off for transient errors
11
+ * - withTimeout — hard deadline on any async operation
12
+ *
13
+ * Run: npm run dev
14
+ * Build: npm run build
15
+ * Start: npm start
16
+ */
17
+
18
+ import express from "express";
19
+ import {
20
+ CircuitBreaker,
21
+ BulkheadPool,
22
+ FallbackCache,
23
+ GracefulShutdown,
24
+ withRetry,
25
+ withTimeout,
26
+ getEventBus,
27
+ } from "@immortal/core";
28
+
29
+ // ─── App ──────────────────────────────────────────────────────────────────────
30
+
31
+ const app = express();
32
+ app.use(express.json());
33
+
34
+ // ─── Immortal.js Primitives ───────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Payment circuit breaker
38
+ * Opens after 5 failures in a 100-request sliding window.
39
+ * Attempts half-open probe after 15s cooldown.
40
+ */
41
+ const paymentCircuit = new CircuitBreaker("payment-provider", {
42
+ threshold: 5,
43
+ cooldownMs: 15_000,
44
+ slidingWindowSize: 100,
45
+ });
46
+
47
+ /**
48
+ * Inventory circuit breaker
49
+ * More sensitive — opens after 3 failures in a 50-request window.
50
+ */
51
+ const inventoryCircuit = new CircuitBreaker("inventory-service", {
52
+ threshold: 3,
53
+ cooldownMs: 10_000,
54
+ slidingWindowSize: 50,
55
+ });
56
+
57
+ /**
58
+ * Payments bulkhead — max 10 concurrent, 20 queued
59
+ * BulkheadRejectedError thrown if both pools are full.
60
+ */
61
+ const paymentsBulkhead = new BulkheadPool("payments", {
62
+ maxConcurrent: 10,
63
+ maxQueueSize: 20,
64
+ queueTimeoutMs: 15_000,
65
+ });
66
+
67
+ /**
68
+ * Search bulkhead — high concurrency allowed (read-only)
69
+ */
70
+ const searchBulkhead = new BulkheadPool("search", {
71
+ maxConcurrent: 50,
72
+ maxQueueSize: 100,
73
+ queueTimeoutMs: 5_000,
74
+ });
75
+
76
+ /**
77
+ * FallbackCache — LRU with TTL
78
+ * Serves stale results when upstream is unavailable.
79
+ */
80
+ const cache = new FallbackCache<unknown>({
81
+ maxSize: 1_000,
82
+ defaultTtlMs: 60_000,
83
+ });
84
+
85
+ // ─── Graceful Shutdown ────────────────────────────────────────────────────────
86
+
87
+ const bus = getEventBus();
88
+
89
+ // ─── Routes ───────────────────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * GET /
93
+ * Welcome + route map
94
+ */
95
+ app.get("/", (_req, res) => {
96
+ res.json({
97
+ name: "Immortal.js Express Basic Example",
98
+ version: "1.0.0",
99
+ status: "running",
100
+ timestamp: new Date().toISOString(),
101
+ routes: {
102
+ "GET /": "This route map",
103
+ "GET /health": "Live health snapshot",
104
+ "GET /status/circuits": "All circuit statuses",
105
+ "POST /payments/charge": "Charge payment [Bulkhead + Circuit + Retry]",
106
+ "GET /inventory/:productId": "Check inventory [Circuit + Cache]",
107
+ "GET /search?q=...": "Search [Bulkhead + Cache]",
108
+ "GET /chaos/trip-payment": "Force payment circuit OPEN (dev)",
109
+ "GET /chaos/reset-circuits": "Reset all circuits (dev)",
110
+ "GET /simulate/crash": "Test global error handler",
111
+ "GET /simulate/leak": "Test MemoryLeakGuard",
112
+ },
113
+ });
114
+ });
115
+
116
+ /**
117
+ * GET /health
118
+ * Live snapshot of all Immortal.js primitive statuses.
119
+ */
120
+ app.get("/health", (_req, res) => {
121
+ const payBk = paymentsBulkhead.getStatus();
122
+ const srchBk = searchBulkhead.getStatus();
123
+
124
+ res.json({
125
+ status: "ok",
126
+ timestamp: new Date().toISOString(),
127
+ uptime: Math.round(process.uptime()),
128
+ circuits: {
129
+ paymentProvider: paymentCircuit.getStatus(),
130
+ inventoryService: inventoryCircuit.getStatus(),
131
+ },
132
+ bulkheads: {
133
+ payments: { active: payBk.active, queued: payBk.queued },
134
+ search: { active: srchBk.active, queued: srchBk.queued },
135
+ },
136
+ cache: {
137
+ description: "FallbackCache (LRU + TTL) — inspect keys via /status/cache",
138
+ },
139
+ });
140
+ });
141
+
142
+ /**
143
+ * POST /payments/charge
144
+ * Body: { amount: number, cardToken: string }
145
+ *
146
+ * Layers:
147
+ * 1. BulkheadPool — cap concurrent payment requests
148
+ * 2. CircuitBreaker — open circuit if gateway repeatedly fails
149
+ * 3. withRetry — retry transient gateway errors
150
+ * 4. withTimeout — hard 10s deadline on the API call
151
+ */
152
+ app.post("/payments/charge", async (req, res, next) => {
153
+ try {
154
+ const result = await paymentsBulkhead.run(
155
+ async () =>
156
+ paymentCircuit.execute(
157
+ async () =>
158
+ withRetry(
159
+ async () => {
160
+ const { amount, cardToken } = req.body as {
161
+ amount: number;
162
+ cardToken: string;
163
+ };
164
+ return withTimeout(
165
+ () => simulatePaymentAPI(amount, cardToken),
166
+ 10_000,
167
+ "payment-api"
168
+ );
169
+ },
170
+ { maxAttempts: 2, baseDelayMs: 300, maxDelayMs: 2_000 }
171
+ ),
172
+ // Circuit-open fallback — inform caller to retry later
173
+ () => ({
174
+ success: false as boolean,
175
+ transactionId: `queued-${Date.now()}`,
176
+ amount: 0,
177
+ queued: true,
178
+ })
179
+ ),
180
+ "high"
181
+ );
182
+
183
+ res.json(result);
184
+ } catch (err) {
185
+ next(err);
186
+ }
187
+ });
188
+
189
+ /**
190
+ * GET /inventory/:productId
191
+ * Circuit Breaker + FallbackCache
192
+ */
193
+ app.get("/inventory/:productId", async (req, res, next) => {
194
+ const { productId } = req.params as { productId: string };
195
+ const cacheKey = `inventory:${productId}`;
196
+
197
+ try {
198
+ const data = await inventoryCircuit.execute(
199
+ async () => {
200
+ const result = await withTimeout(
201
+ () => simulateInventoryAPI(productId),
202
+ 5_000,
203
+ "inventory-api"
204
+ );
205
+ // Cache successful response
206
+ cache.set(cacheKey, result, 30_000);
207
+ return result;
208
+ },
209
+ // Circuit-open fallback — serve stale cache or empty
210
+ () => {
211
+ const stale = cache.get(cacheKey);
212
+ if (stale) {
213
+ res.setHeader("X-Immortal-Fallback", "stale-cache");
214
+ return stale;
215
+ }
216
+ return { productId, stock: 0, stale: true, circuitOpen: true };
217
+ }
218
+ );
219
+
220
+ res.json(data);
221
+ } catch (err) {
222
+ next(err);
223
+ }
224
+ });
225
+
226
+ /**
227
+ * GET /search?q=...
228
+ * Bulkhead + FallbackCache (1-minute TTL)
229
+ */
230
+ app.get("/search", async (req, res, next) => {
231
+ try {
232
+ const q = (req.query["q"] as string | undefined) ?? "";
233
+ const cacheKey = `search:${q}`;
234
+
235
+ const result = await searchBulkhead.run(async () => {
236
+ const cached = cache.get(cacheKey);
237
+ if (cached) {
238
+ res.setHeader("X-Cache", "HIT");
239
+ return cached;
240
+ }
241
+
242
+ const results = await simulateSearchAPI(q);
243
+ cache.set(cacheKey, results, 60_000);
244
+ return results;
245
+ }, "medium");
246
+
247
+ res.json(result);
248
+ } catch (err) {
249
+ next(err);
250
+ }
251
+ });
252
+
253
+ /**
254
+ * GET /status/circuits
255
+ * All circuit breaker statuses (open/closed/half-open)
256
+ */
257
+ app.get("/status/circuits", (_req, res) => {
258
+ res.json({
259
+ paymentProvider: paymentCircuit.getStatus(),
260
+ inventoryService: inventoryCircuit.getStatus(),
261
+ });
262
+ });
263
+
264
+ /**
265
+ * GET /chaos/trip-payment
266
+ * Force payment circuit OPEN (dev / load-test use only)
267
+ */
268
+ app.get("/chaos/trip-payment", (_req, res) => {
269
+ if (process.env["NODE_ENV"] === "production") {
270
+ res.status(403).json({ error: "Chaos endpoints disabled in production" });
271
+ return;
272
+ }
273
+ paymentCircuit.forceState("OPEN");
274
+ res.json({
275
+ message: "Payment circuit forced OPEN",
276
+ state: paymentCircuit.getStatus().state,
277
+ });
278
+ });
279
+
280
+ /**
281
+ * GET /chaos/reset-circuits
282
+ * Reset all circuits to CLOSED
283
+ */
284
+ app.get("/chaos/reset-circuits", (_req, res) => {
285
+ if (process.env["NODE_ENV"] === "production") {
286
+ res.status(403).json({ error: "Chaos endpoints disabled in production" });
287
+ return;
288
+ }
289
+ paymentCircuit.forceState("CLOSED");
290
+ inventoryCircuit.forceState("CLOSED");
291
+ res.json({
292
+ message: "All circuits reset to CLOSED",
293
+ payment: paymentCircuit.getStatus().state,
294
+ inventory: inventoryCircuit.getStatus().state,
295
+ });
296
+ });
297
+
298
+ /**
299
+ * GET /simulate/crash
300
+ * Intentional error — exercises the global error handler.
301
+ */
302
+ app.get("/simulate/crash", (_req, _res, next) => {
303
+ next(new Error("Simulated crash — global error handler should catch this!"));
304
+ });
305
+
306
+ /**
307
+ * GET /simulate/leak
308
+ * Intentional memory leak — exercises MemoryLeakGuard.
309
+ */
310
+ const leakyArray: Buffer[] = [];
311
+ app.get("/simulate/leak", (_req, res) => {
312
+ leakyArray.push(Buffer.alloc(1024 * 1024)); // 1 MB per request
313
+ res.json({
314
+ message: "Memory leaked (1 MB)",
315
+ totalLeakedMb: leakyArray.length,
316
+ note: "MemoryLeakGuard detects heap growth trends and initiates proactive restart",
317
+ });
318
+ });
319
+
320
+ // ─── Global Error Handler ─────────────────────────────────────────────────────
321
+
322
+ app.use(
323
+ (
324
+ err: unknown,
325
+ _req: express.Request,
326
+ res: express.Response,
327
+ _next: express.NextFunction
328
+ ) => {
329
+ if (res.headersSent) return;
330
+
331
+ const status =
332
+ (err as { statusCode?: number; status?: number }).statusCode ??
333
+ (err as { statusCode?: number; status?: number }).status ??
334
+ 500;
335
+
336
+ const name = (err as { name?: string }).name ?? "Error";
337
+
338
+ // Translate Immortal.js-specific error names to HTTP status codes
339
+ const httpStatus =
340
+ name === "BulkheadRejectedError"
341
+ ? 429
342
+ : name === "TimeoutError"
343
+ ? 504
344
+ : status;
345
+
346
+ res.status(httpStatus).json({
347
+ error:
348
+ httpStatus >= 500
349
+ ? "Internal Server Error"
350
+ : (err as Error).message ?? "Unknown error",
351
+ errorType: name,
352
+ timestamp: new Date().toISOString(),
353
+ });
354
+ }
355
+ );
356
+
357
+ // ─── Start ────────────────────────────────────────────────────────────────────
358
+
359
+ const PORT = parseInt(process.env["PORT"] ?? "3000", 10);
360
+
361
+ const server = app.listen(PORT, () => {
362
+ console.log(`
363
+ ╔══════════════════════════════════════════════════════════════╗
364
+ ║ Immortal.js — Express Basic Example 🚀 ║
365
+ ╠══════════════════════════════════════════════════════════════╣
366
+ ║ Server → http://localhost:${PORT} ║
367
+ ║ Health → http://localhost:${PORT}/health ║
368
+ ║ Circuits → http://localhost:${PORT}/status/circuits ║
369
+ ╠══════════════════════════════════════════════════════════════╣
370
+ ║ Routes ║
371
+ ║ POST /payments/charge Bulkhead + Circuit + Retry ║
372
+ ║ GET /inventory/:productId Circuit + FallbackCache ║
373
+ ║ GET /search?q=... Bulkhead + FallbackCache ║
374
+ ║ GET /chaos/trip-payment Force payment circuit OPEN ║
375
+ ║ GET /chaos/reset-circuits Reset all circuits ║
376
+ ║ GET /simulate/crash Test global error handler ║
377
+ ║ GET /simulate/leak Test MemoryLeakGuard ║
378
+ ╚══════════════════════════════════════════════════════════════╝
379
+ `);
380
+ });
381
+
382
+ // Register graceful shutdown — drains server before exit
383
+ const shutdown = new GracefulShutdown(
384
+ {
385
+ timeoutMs: 20_000,
386
+ signals: ["SIGTERM", "SIGINT"],
387
+ onShutdown: [
388
+ async (_signal: string) => {
389
+ // Drain the payments bulkhead queue
390
+ paymentsBulkhead.drain();
391
+ // Close HTTP server (stop accepting new requests)
392
+ await new Promise<void>((resolve, reject) => {
393
+ server.close((err) => (err ? reject(err) : resolve()));
394
+ });
395
+ },
396
+ ],
397
+ },
398
+ bus
399
+ );
400
+
401
+ // Keep reference alive (signal handlers are installed in constructor)
402
+ void shutdown;
403
+
404
+ // ─── Simulated External Services ─────────────────────────────────────────────
405
+
406
+ async function simulatePaymentAPI(
407
+ amount: number,
408
+ _cardToken: string
409
+ ): Promise<{ success: boolean; transactionId: string; amount: number }> {
410
+ // 20% failure rate — exercises retry + circuit breaker
411
+ if (Math.random() < 0.2) {
412
+ const err = Object.assign(new Error("Payment gateway timeout"), {
413
+ statusCode: 503,
414
+ });
415
+ throw err;
416
+ }
417
+
418
+ await sleep(100 + Math.random() * 200);
419
+
420
+ return {
421
+ success: true,
422
+ transactionId: `txn_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
423
+ amount,
424
+ };
425
+ }
426
+
427
+ async function simulateInventoryAPI(
428
+ productId: string
429
+ ): Promise<{ productId: string; stock: number; warehouseId: string }> {
430
+ // 10% failure rate
431
+ if (Math.random() < 0.1) {
432
+ throw new Error("Inventory service unavailable");
433
+ }
434
+
435
+ await sleep(30 + Math.random() * 70);
436
+
437
+ return {
438
+ productId,
439
+ stock: Math.floor(Math.random() * 200),
440
+ warehouseId: `WH-${Math.floor(Math.random() * 5) + 1}`,
441
+ };
442
+ }
443
+
444
+ async function simulateSearchAPI(
445
+ query: string
446
+ ): Promise<{ results: Array<{ id: number; title: string; score: number }> }> {
447
+ await sleep(20 + Math.random() * 50);
448
+
449
+ return {
450
+ results: Array.from({ length: 5 }, (_, i) => ({
451
+ id: i + 1,
452
+ title: `Result ${i + 1} for "${query}"`,
453
+ score: Math.round(Math.random() * 100) / 100,
454
+ })).sort((a, b) => b.score - a.score),
455
+ };
456
+ }
457
+
458
+ function sleep(ms: number): Promise<void> {
459
+ return new Promise((resolve) => setTimeout(resolve, ms));
460
+ }
461
+
462
+ export { app };
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "immortal-express-basic-example",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "node --import tsx/esm --no-warnings index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@immortal/core": "*",
13
+ "express": "^4.18.3"
14
+ },
15
+ "devDependencies": {
16
+ "@types/express": "^4.17.21",
17
+ "@types/node": "^20.0.0",
18
+ "tsx": "^4.7.0",
19
+ "typescript": "^5.4.0"
20
+ }
21
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "strictNullChecks": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "noImplicitOverride": true,
12
+ "noPropertyAccessFromIndexSignature": true,
13
+ "declaration": false,
14
+ "sourceMap": true,
15
+ "esModuleInterop": true,
16
+ "allowSyntheticDefaultImports": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "skipLibCheck": true,
19
+ "outDir": "dist",
20
+ "rootDir": "./"
21
+ },
22
+ "include": ["index.ts"],
23
+ "exclude": ["node_modules", "dist"]
24
+ }