@olane/o-node 0.7.12-alpha.3 → 0.7.12-alpha.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAIV,MAAM,EAEP,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF,qBAAa,eAAgB,SAAQ,WAAW;IAGlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAFrD,aAAa,EAAE,UAAU,CAAC;gBAEF,MAAM,EAAE,qBAAqB;IAKtD,IAAI,CAAC,MAAM,EAAE,MAAM;IAWzB,QAAQ;IAOF,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAyC/C,KAAK;CAKZ"}
1
+ {"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAIV,MAAM,EAEP,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF,qBAAa,eAAgB,SAAQ,WAAW;IAGlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAFrD,aAAa,EAAE,UAAU,CAAC;gBAEF,MAAM,EAAE,qBAAqB;IAKtD,IAAI,CAAC,MAAM,EAAE,MAAM;IAWzB,QAAQ;IAOF,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAiD/C,KAAK;CAKZ"}
@@ -33,8 +33,14 @@ export class oNodeConnection extends oConnection {
33
33
  if (stream.status === 'reset') {
34
34
  throw new oError(oErrorCodes.CONNECTION_LIMIT_REACHED, 'Connection limit reached');
35
35
  }
36
- // Send the data
37
- await stream.send(new TextEncoder().encode(request.toString()));
36
+ // Send the data with backpressure handling (libp2p v3 best practice)
37
+ const data = new TextEncoder().encode(request.toString());
38
+ const sent = stream.send(data);
39
+ // If send() returns false, wait for the stream to drain before continuing
40
+ if (!sent) {
41
+ this.logger.debug('Stream buffer full, waiting for drain...');
42
+ await stream.onDrain({ signal: AbortSignal.timeout(30000) }); // 30 second timeout
43
+ }
38
44
  const res = await this.read(stream);
39
45
  await stream.close();
40
46
  // process the response
@@ -6,7 +6,7 @@ export declare class oNodeConnectionManager extends oConnectionManager {
6
6
  private p2pNode;
7
7
  constructor(config: oNodeConnectionManagerConfig);
8
8
  /**
9
- * Connect to a given address
9
+ * Connect to a given address with exponential backoff retry
10
10
  * @param address - The address to connect to
11
11
  * @returns The connection object
12
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"o-node-connection.manager.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,EAAE,4BAA4B,EAAE,MAAM,kDAAkD,CAAC;AAEhG,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,qBAAa,sBAAuB,SAAQ,kBAAkB;IAC5D,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,EAAE,4BAA4B;IAKhD;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IAkDlE,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IAIpC,mBAAmB,CAAC,OAAO,EAAE,QAAQ,GAAG,WAAW,GAAG,IAAI;CAe3D"}
1
+ {"version":3,"file":"o-node-connection.manager.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,EAAE,4BAA4B,EAAE,MAAM,kDAAkD,CAAC;AAEhG,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,qBAAa,sBAAuB,SAAQ,kBAAkB;IAC5D,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,EAAE,4BAA4B;IAKhD;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IAqFlE,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IAIpC,mBAAmB,CAAC,OAAO,EAAE,QAAQ,GAAG,WAAW,GAAG,IAAI;CAe3D"}
@@ -6,7 +6,7 @@ export class oNodeConnectionManager extends oConnectionManager {
6
6
  this.p2pNode = config.p2pNode;
7
7
  }
8
8
  /**
9
- * Connect to a given address
9
+ * Connect to a given address with exponential backoff retry
10
10
  * @param address - The address to connect to
11
11
  * @returns The connection object
12
12
  */
@@ -26,25 +26,45 @@ export class oNodeConnectionManager extends oConnectionManager {
26
26
  this.cache.delete(nextHopAddress.toString());
27
27
  }
28
28
  }
29
- // first time setup connection
30
- try {
31
- const p2pConnection = await this.p2pNode.dial(nextHopAddress.libp2pTransports.map((ma) => ma.toMultiaddr()));
32
- const connection = new oNodeConnection({
33
- nextHopAddress: nextHopAddress,
34
- address: address,
35
- p2pConnection: p2pConnection,
36
- callerAddress: callerAddress,
37
- });
38
- // this.cache.set(nextHopAddress.toString(), connection);
39
- return connection;
40
- }
41
- catch (error) {
42
- this.logger.error(`[${callerAddress?.toString() || 'unknown'}] Error connecting to address! Next hop:` +
43
- nextHopAddress +
44
- ' With Address:' +
45
- address.toString(), error);
46
- throw error;
29
+ // Retry configuration for handling transient connection failures
30
+ const MAX_RETRIES = 3;
31
+ const BASE_DELAY_MS = 1000; // Start with 1 second
32
+ const MAX_DELAY_MS = 10000; // Cap at 10 seconds
33
+ // first time setup connection with retry logic
34
+ let lastError;
35
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
36
+ try {
37
+ if (attempt > 0) {
38
+ // Calculate exponential backoff delay: 1s, 2s, 4s, 8s (capped at MAX_DELAY_MS)
39
+ const delay = Math.min(BASE_DELAY_MS * Math.pow(2, attempt - 1), MAX_DELAY_MS);
40
+ this.logger.debug(`Retry attempt ${attempt}/${MAX_RETRIES} for ${nextHopAddress.toString()} after ${delay}ms delay`);
41
+ await new Promise((resolve) => setTimeout(resolve, delay));
42
+ }
43
+ const p2pConnection = await this.p2pNode.dial(nextHopAddress.libp2pTransports.map((ma) => ma.toMultiaddr()));
44
+ const connection = new oNodeConnection({
45
+ nextHopAddress: nextHopAddress,
46
+ address: address,
47
+ p2pConnection: p2pConnection,
48
+ callerAddress: callerAddress,
49
+ });
50
+ if (attempt > 0) {
51
+ this.logger.info(`Successfully connected to ${nextHopAddress.toString()} on retry attempt ${attempt}`);
52
+ }
53
+ // this.cache.set(nextHopAddress.toString(), connection);
54
+ return connection;
55
+ }
56
+ catch (error) {
57
+ lastError = error;
58
+ this.logger.warn(`[${callerAddress?.toString() || 'unknown'}] Connection attempt ${attempt + 1}/${MAX_RETRIES + 1} failed for ${nextHopAddress.toString()}: ${error instanceof Error ? error.message : String(error)}`);
59
+ // Don't retry on the last attempt
60
+ if (attempt === MAX_RETRIES) {
61
+ break;
62
+ }
63
+ }
47
64
  }
65
+ // All retries exhausted
66
+ this.logger.error(`[${callerAddress?.toString() || 'unknown'}] Failed to connect after ${MAX_RETRIES + 1} attempts to address! Next hop: ${nextHopAddress} With Address: ${address.toString()}`, lastError);
67
+ throw lastError;
48
68
  }
49
69
  isCached(address) {
50
70
  return this.cache.has(address.toString());
@@ -1 +1 @@
1
- {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAGR,QAAQ,EAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAGrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IACzC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAQhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CnE,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAa5D"}
1
+ {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAGR,QAAQ,EAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAGrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IACzC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAQhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDnE,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAa5D"}
@@ -24,7 +24,10 @@ export class oNodeTool extends oTool(oServerNode) {
24
24
  }
25
25
  }
26
26
  async handleStream(stream, connection) {
27
- stream.addEventListener('message', async (event) => {
27
+ // CRITICAL: Attach message listener immediately to prevent buffer overflow (libp2p v3)
28
+ // Per libp2p migration guide: "If no message event handler is added, streams will
29
+ // buffer incoming data until a pre-configured limit is reached, after which the stream will be reset."
30
+ const messageHandler = async (event) => {
28
31
  if (!event.data) {
29
32
  this.logger.warn('Malformed event data');
30
33
  return;
@@ -52,7 +55,9 @@ export class oNodeTool extends oTool(oServerNode) {
52
55
  const response = CoreUtils.buildResponse(request, result, result?.error);
53
56
  // add the request method to the response
54
57
  await CoreUtils.sendResponse(response, stream);
55
- });
58
+ };
59
+ // Attach listener synchronously before any async operations
60
+ stream.addEventListener('message', messageHandler);
56
61
  }
57
62
  async _tool_identify() {
58
63
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"o-node.search-resolver.d.ts","sourceRoot":"","sources":["../../../../src/router/resolvers/o-node.search-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,KAAK,EAEL,UAAU,EACV,cAAc,EAEd,aAAa,EAEd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;IACvC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ;gBAAjB,OAAO,EAAE,QAAQ;IAIhD,IAAI,gBAAgB,IAAI,UAAU,EAAE,CAEnC;IAED;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,IAAI,QAAQ;IAIxC;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;IAInC;;;;;OAKG;IACH,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,GAAG,GAAG;IAOnD;;;;;;OAMG;IACH,SAAS,CAAC,mBAAmB,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,GAAG,GAAG,EAAE;IASjE;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI;IAIlD;;;;;OAKG;IACH,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,GAAG,GAAG,cAAc,EAAE;IAOtD;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,wBAAwB,CAChC,OAAO,EAAE,QAAQ,EACjB,gBAAgB,EAAE,cAAc,EAAE,EAClC,IAAI,EAAE,KAAK,GACV,cAAc,EAAE;IAgBnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACH,SAAS,CAAC,gBAAgB,CACxB,IAAI,EAAE,KAAK,EACX,qBAAqB,EAAE,QAAQ,EAC/B,YAAY,EAAE,GAAG,GAChB,QAAQ;IAsBL,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CA8D/D"}
1
+ {"version":3,"file":"o-node.search-resolver.d.ts","sourceRoot":"","sources":["../../../../src/router/resolvers/o-node.search-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,KAAK,EAEL,UAAU,EACV,cAAc,EAEd,aAAa,EAEd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;IACvC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ;gBAAjB,OAAO,EAAE,QAAQ;IAIhD,IAAI,gBAAgB,IAAI,UAAU,EAAE,CAEnC;IAED;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,IAAI,QAAQ;IAIxC;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;IAInC;;;;;OAKG;IACH,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,GAAG,GAAG;IAOnD;;;;;;OAMG;IACH,SAAS,CAAC,mBAAmB,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,GAAG,GAAG,EAAE;IASjE;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI;IAIlD;;;;;OAKG;IACH,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,GAAG,GAAG,cAAc,EAAE;IAOtD;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,wBAAwB,CAChC,OAAO,EAAE,QAAQ,EACjB,gBAAgB,EAAE,cAAc,EAAE,EAClC,IAAI,EAAE,KAAK,GACV,cAAc,EAAE;IAgBnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACH,SAAS,CAAC,gBAAgB,CACxB,IAAI,EAAE,KAAK,EACX,qBAAqB,EAAE,QAAQ,EAC/B,YAAY,EAAE,GAAG,GAChB,QAAQ;IAsBL,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CAoE/D"}
@@ -241,7 +241,12 @@ export class oSearchResolver extends oAddressResolver {
241
241
  .toString() // o://embeddings-text replace o://embeddings-text = ''
242
242
  .replace(address.toRootAddress().toString(), '');
243
243
  this.logger.debug('Extra params:', extraParams);
244
- const resolvedTargetAddress = new oAddress(selectedResult.address + extraParams);
244
+ // Check if selectedResult.address already contains the complete path
245
+ // This happens when registry finds via staticAddress - the returned address
246
+ // is the canonical hierarchical location, so we shouldn't append extraParams
247
+ const resultAddress = selectedResult.address;
248
+ const shouldAppendParams = extraParams && !resultAddress.endsWith(extraParams);
249
+ const resolvedTargetAddress = new oAddress(shouldAppendParams ? resultAddress + extraParams : resultAddress);
245
250
  // Set transports on the target address
246
251
  resolvedTargetAddress.setTransports(this.mapTransports(selectedResult));
247
252
  // Determine next hop and configure transports
@@ -515,6 +515,85 @@ describe('oSearchResolver', () => {
515
515
  }
516
516
  });
517
517
  });
518
+ describe('Address duplication bug fix', () => {
519
+ it('should NOT duplicate path segments when registry returns full hierarchical address', async () => {
520
+ // This test verifies the fix for the bug where o://services/embeddings-text
521
+ // was being resolved to o://leader/services/embeddings-text/services/embeddings-text
522
+ const address = new oNodeAddress('o://services/embeddings-text');
523
+ mockNode.use = async () => createResponse({
524
+ data: [
525
+ {
526
+ address: 'o://leader/services/embeddings-text',
527
+ staticAddress: 'o://embeddings-text',
528
+ transports: [
529
+ {
530
+ value: '/ip4/127.0.0.1/tcp/6001',
531
+ type: TransportType.LIBP2P,
532
+ },
533
+ ],
534
+ },
535
+ ],
536
+ });
537
+ const result = await resolver.resolve({
538
+ address,
539
+ targetAddress: address,
540
+ node: mockNode,
541
+ request: createRouterRequest(),
542
+ });
543
+ // Should be o://leader/services/embeddings-text, NOT o://leader/services/embeddings-text/embeddings-text
544
+ expect(result.targetAddress.value).to.equal('o://leader/services/embeddings-text');
545
+ });
546
+ it('should NOT duplicate when calling via static address', async () => {
547
+ const address = new oNodeAddress('o://embeddings-text');
548
+ mockNode.use = async () => createResponse({
549
+ data: [
550
+ {
551
+ address: 'o://leader/services/embeddings-text',
552
+ staticAddress: 'o://embeddings-text',
553
+ transports: [
554
+ {
555
+ value: '/ip4/127.0.0.1/tcp/6001',
556
+ type: TransportType.LIBP2P,
557
+ },
558
+ ],
559
+ },
560
+ ],
561
+ });
562
+ const result = await resolver.resolve({
563
+ address,
564
+ targetAddress: address,
565
+ node: mockNode,
566
+ request: createRouterRequest(),
567
+ });
568
+ expect(result.targetAddress.value).to.equal('o://leader/services/embeddings-text');
569
+ });
570
+ it('should still append legitimate extra params beyond the service name', async () => {
571
+ // If someone calls o://embeddings-text/custom/path, we should preserve /custom/path
572
+ const address = new oNodeAddress('o://embeddings-text/custom/path');
573
+ mockNode.use = async () => createResponse({
574
+ data: [
575
+ {
576
+ address: 'o://leader/services/embeddings-text',
577
+ staticAddress: 'o://embeddings-text',
578
+ transports: [
579
+ {
580
+ value: '/ip4/127.0.0.1/tcp/6001',
581
+ type: TransportType.LIBP2P,
582
+ },
583
+ ],
584
+ },
585
+ ],
586
+ });
587
+ const result = await resolver.resolve({
588
+ address,
589
+ targetAddress: address,
590
+ node: mockNode,
591
+ request: createRouterRequest(),
592
+ });
593
+ // Should append the extra /custom/path
594
+ expect(result.targetAddress.value).to.equal('o://leader/services/embeddings-text/custom/path');
595
+ });
596
+ });
518
597
  describe('Edge cases', () => {
519
598
  it('should handle address with long nested paths', async () => {
520
599
  const address = new oNodeAddress('o://leader/a/b/c/d/e/f');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olane/o-node",
3
- "version": "0.7.12-alpha.3",
3
+ "version": "0.7.12-alpha.5",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -54,12 +54,12 @@
54
54
  "typescript": "5.4.5"
55
55
  },
56
56
  "dependencies": {
57
- "@olane/o-config": "^0.7.12-alpha.3",
58
- "@olane/o-core": "0.7.12-alpha.3",
59
- "@olane/o-protocol": "^0.7.12-alpha.3",
60
- "@olane/o-tool": "0.7.12-alpha.3",
57
+ "@olane/o-config": "0.7.12-alpha.5",
58
+ "@olane/o-core": "0.7.12-alpha.4",
59
+ "@olane/o-protocol": "0.7.12-alpha.4",
60
+ "@olane/o-tool": "0.7.12-alpha.5",
61
61
  "debug": "^4.4.1",
62
62
  "dotenv": "^16.5.0"
63
63
  },
64
- "gitHead": "5c05e5f248001abc38f34c77bd97c9a325231503"
64
+ "gitHead": "f0b799131f025624da04374b76e9aa373402f787"
65
65
  }