@sourceregistry/node-ovsdb 1.0.2 → 1.0.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.
@@ -0,0 +1,742 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.OVSDBClient = exports.OvsdbTransaction = exports.OvsdbTransactionError = exports.OvsdbProtocolError = exports.OvsdbRpcError = void 0;
18
+ exports.resolveConnectionOptions = resolveConnectionOptions;
19
+ const node_fs_1 = require("node:fs");
20
+ const node_net_1 = require("node:net");
21
+ const node_events_1 = require("node:events");
22
+ const node_tls_1 = require("node:tls");
23
+ /**
24
+ * JSON-RPC transport error returned by the OVSDB server.
25
+ */
26
+ class OvsdbRpcError extends Error {
27
+ /**
28
+ * The error payload returned by the server.
29
+ */
30
+ response;
31
+ /**
32
+ * Creates a new RPC error wrapper.
33
+ */
34
+ constructor(response) {
35
+ super(response.details ? `${response.error}: ${response.details}` : response.error);
36
+ this.name = "OvsdbRpcError";
37
+ this.response = response;
38
+ }
39
+ }
40
+ exports.OvsdbRpcError = OvsdbRpcError;
41
+ /**
42
+ * Raised when a message does not match the expected JSON-RPC envelope.
43
+ */
44
+ class OvsdbProtocolError extends Error {
45
+ /**
46
+ * The raw payload that failed validation.
47
+ */
48
+ payload;
49
+ /**
50
+ * Creates a new protocol error wrapper.
51
+ */
52
+ constructor(message, payload) {
53
+ super(message);
54
+ this.name = "OvsdbProtocolError";
55
+ this.payload = payload;
56
+ }
57
+ }
58
+ exports.OvsdbProtocolError = OvsdbProtocolError;
59
+ /**
60
+ * Raised when an OVSDB transaction response contains an operation-level error.
61
+ */
62
+ class OvsdbTransactionError extends Error {
63
+ /**
64
+ * Zero-based index of the failed operation in the submitted transaction.
65
+ */
66
+ operationIndex;
67
+ /**
68
+ * Operation that produced the error.
69
+ */
70
+ operation;
71
+ /**
72
+ * OVSDB error payload returned for the failed operation.
73
+ */
74
+ result;
75
+ /**
76
+ * Raw transaction results returned by the server.
77
+ */
78
+ results;
79
+ /**
80
+ * Creates a new transaction error wrapper.
81
+ */
82
+ constructor(options) {
83
+ super(`Transaction operation ${options.operationIndex} failed: ${options.result.error}`);
84
+ this.name = "OvsdbTransactionError";
85
+ this.operationIndex = options.operationIndex;
86
+ this.operation = options.operation;
87
+ this.result = options.result;
88
+ this.results = options.results;
89
+ }
90
+ }
91
+ exports.OvsdbTransactionError = OvsdbTransactionError;
92
+ /**
93
+ * Stages OVSDB operations before sending them as a single `transact` request.
94
+ */
95
+ class OvsdbTransaction {
96
+ stagedOperations = [];
97
+ /**
98
+ * Returns the currently staged operations.
99
+ */
100
+ get operations() {
101
+ return this.stagedOperations;
102
+ }
103
+ /**
104
+ * Adds an operation to the transaction.
105
+ */
106
+ add(operation) {
107
+ this.stagedOperations.push(operation);
108
+ return operation;
109
+ }
110
+ /**
111
+ * Stages an insert operation.
112
+ */
113
+ insert(operation) {
114
+ return this.add(operation);
115
+ }
116
+ /**
117
+ * Stages a select operation.
118
+ */
119
+ select(operation) {
120
+ return this.add(operation);
121
+ }
122
+ /**
123
+ * Stages an update operation.
124
+ */
125
+ update(operation) {
126
+ return this.add(operation);
127
+ }
128
+ /**
129
+ * Stages a mutate operation.
130
+ */
131
+ mutate(operation) {
132
+ return this.add(operation);
133
+ }
134
+ /**
135
+ * Stages a delete operation.
136
+ */
137
+ delete(operation) {
138
+ return this.add(operation);
139
+ }
140
+ /**
141
+ * Stages a wait operation.
142
+ */
143
+ wait(operation) {
144
+ return this.add(operation);
145
+ }
146
+ /**
147
+ * Stages a comment operation.
148
+ */
149
+ comment(comment) {
150
+ return this.add({
151
+ op: "comment",
152
+ comment
153
+ });
154
+ }
155
+ /**
156
+ * Stages an assert operation.
157
+ */
158
+ assert(lock) {
159
+ return this.add({
160
+ op: "assert",
161
+ lock
162
+ });
163
+ }
164
+ /**
165
+ * Stages a commit operation.
166
+ */
167
+ commit(durable = false) {
168
+ return this.add({
169
+ op: "commit",
170
+ durable
171
+ });
172
+ }
173
+ /**
174
+ * Stages an abort operation.
175
+ */
176
+ abort() {
177
+ return this.add({
178
+ op: "abort"
179
+ });
180
+ }
181
+ }
182
+ exports.OvsdbTransaction = OvsdbTransaction;
183
+ /**
184
+ * A low-level, event-driven OVSDB client for Unix sockets, TCP, or TLS.
185
+ *
186
+ * The client exposes the RFC 7047 primitives together with common
187
+ * Open vSwitch protocol extensions while keeping the API small and predictable.
188
+ */
189
+ class OVSDBClient extends node_events_1.EventEmitter {
190
+ timeout;
191
+ connectionOptions;
192
+ connectionFactory;
193
+ socket = null;
194
+ requestId = 1;
195
+ receiveBuffer = "";
196
+ pendingRequests = new Map();
197
+ connected = false;
198
+ closeEmitted = false;
199
+ /**
200
+ * Creates a new OVSDB client instance.
201
+ */
202
+ constructor(options = {}) {
203
+ super();
204
+ this.timeout = options.timeout ?? 5000;
205
+ this.connectionOptions = resolveConnectionOptions(options);
206
+ this.connectionFactory = options.connectionFactory;
207
+ }
208
+ /**
209
+ * Returns `true` when the underlying socket is currently connected.
210
+ */
211
+ get isConnected() {
212
+ return this.connected;
213
+ }
214
+ /**
215
+ * Opens the transport connection.
216
+ *
217
+ * @returns The connected client instance for chaining.
218
+ */
219
+ async connect() {
220
+ if (this.connected) {
221
+ return this;
222
+ }
223
+ if (!this.connectionFactory &&
224
+ this.connectionOptions.transport === "unix" &&
225
+ !(0, node_fs_1.existsSync)(this.connectionOptions.socketPath)) {
226
+ throw new Error(`OVSDB socket not found: ${this.connectionOptions.socketPath}`);
227
+ }
228
+ const socket = this.connectionFactory
229
+ ? this.connectionFactory(this.connectionOptions)
230
+ : createTransport(this.connectionOptions);
231
+ this.attachSocket(socket);
232
+ const connectEvent = this.connectionOptions.transport === "tls" ? "secureConnect" : "connect";
233
+ await new Promise((resolve, reject) => {
234
+ const timeoutId = setTimeout(() => {
235
+ cleanup();
236
+ this.disposeTransport(new Error(`Connection timeout after ${this.timeout}ms`));
237
+ reject(new Error(`Connection timeout after ${this.timeout}ms`));
238
+ }, this.timeout);
239
+ const onConnect = () => {
240
+ cleanup();
241
+ this.connected = true;
242
+ this.closeEmitted = false;
243
+ this.emit("connect");
244
+ resolve();
245
+ };
246
+ const onError = (error) => {
247
+ cleanup();
248
+ reject(error);
249
+ };
250
+ const cleanup = () => {
251
+ clearTimeout(timeoutId);
252
+ socket.off(connectEvent, onConnect);
253
+ socket.off("error", onError);
254
+ };
255
+ socket.once(connectEvent, onConnect);
256
+ socket.once("error", onError);
257
+ });
258
+ return this;
259
+ }
260
+ /**
261
+ * Sends a raw JSON-RPC request and resolves with its `result` payload.
262
+ *
263
+ * @param method RPC method name.
264
+ * @param params RPC parameters.
265
+ */
266
+ async request(method, params = []) {
267
+ this.assertConnected();
268
+ const id = this.requestId++;
269
+ const payload = {
270
+ method,
271
+ params,
272
+ id
273
+ };
274
+ return await new Promise((resolve, reject) => {
275
+ const timeoutId = setTimeout(() => {
276
+ this.pendingRequests.delete(id);
277
+ reject(new Error(`Request timeout for method: ${method}`));
278
+ }, this.timeout);
279
+ this.pendingRequests.set(id, {
280
+ resolve: (value) => resolve(value),
281
+ reject,
282
+ timeoutId
283
+ });
284
+ this.writeMessage(payload).catch((error) => {
285
+ clearTimeout(timeoutId);
286
+ this.pendingRequests.delete(id);
287
+ reject(error);
288
+ });
289
+ });
290
+ }
291
+ /**
292
+ * Sends a JSON-RPC notification without waiting for a response.
293
+ *
294
+ * @param method RPC method name.
295
+ * @param params RPC parameters.
296
+ */
297
+ async notify(method, params = []) {
298
+ this.assertConnected();
299
+ await this.writeMessage({ method, params });
300
+ }
301
+ /**
302
+ * Returns the database names exposed by the connected OVSDB server.
303
+ */
304
+ async listDbs() {
305
+ return await this.request("list_dbs", []);
306
+ }
307
+ /**
308
+ * Returns the schema definition for a database.
309
+ *
310
+ * @param dbName Database name.
311
+ */
312
+ async getSchema(dbName = "Open_vSwitch") {
313
+ return await this.request("get_schema", [dbName]);
314
+ }
315
+ /**
316
+ * Executes a single transaction.
317
+ *
318
+ * The response preserves operation ordering, so tuple inputs infer tuple outputs.
319
+ *
320
+ * @param dbName Database name.
321
+ * @param operations Transaction operations.
322
+ */
323
+ async transact(dbName, operations) {
324
+ return await this.request("transact", [dbName, ...operations]);
325
+ }
326
+ /**
327
+ * Stages a transaction in a callback and submits it only if the callback
328
+ * completes successfully.
329
+ *
330
+ * This helper is convenient when you want a scoped, imperative API while
331
+ * still sending exactly one OVSDB `transact` request.
332
+ *
333
+ * @param dbName Database name.
334
+ * @param callback Callback that stages operations on the transaction object.
335
+ * @param options Auto-commit behavior for the staged transaction.
336
+ */
337
+ async transaction(dbName, callback, options = {}) {
338
+ const transaction = new OvsdbTransaction();
339
+ const value = await callback(transaction);
340
+ const operations = [...transaction.operations];
341
+ const autoCommit = options.autoCommit ?? true;
342
+ const hasFinalizer = operations.some((operation) => operation.op === "commit" || operation.op === "abort");
343
+ if (autoCommit && !hasFinalizer) {
344
+ operations.push({
345
+ op: "commit",
346
+ durable: options.durable ?? false
347
+ });
348
+ }
349
+ if (operations.length === 0) {
350
+ return {
351
+ value,
352
+ operations,
353
+ results: []
354
+ };
355
+ }
356
+ const results = await this.request("transact", [dbName, ...operations]);
357
+ for (const [index, result] of results.entries()) {
358
+ if (isOvsdbError(result)) {
359
+ throw new OvsdbTransactionError({
360
+ operationIndex: index,
361
+ operation: operations[index],
362
+ result,
363
+ results
364
+ });
365
+ }
366
+ }
367
+ return {
368
+ value,
369
+ operations,
370
+ results: results
371
+ };
372
+ }
373
+ /**
374
+ * Cancels a previously issued request by id.
375
+ *
376
+ * @param requestId JSON-RPC request id to cancel.
377
+ */
378
+ async cancel(requestId) {
379
+ return await this.request("cancel", [requestId]);
380
+ }
381
+ /**
382
+ * Starts a standard RFC 7047 monitor.
383
+ *
384
+ * @param dbName Database name.
385
+ * @param monitorId Application-defined monitor id.
386
+ * @param monitorRequests Per-table monitor definitions.
387
+ */
388
+ async monitor(dbName, monitorId, monitorRequests) {
389
+ return await this.request("monitor", [
390
+ dbName,
391
+ monitorId,
392
+ monitorRequests
393
+ ]);
394
+ }
395
+ /**
396
+ * Starts an Open vSwitch conditional monitor.
397
+ *
398
+ * @param dbName Database name.
399
+ * @param monitorId Application-defined monitor id.
400
+ * @param monitorRequests Per-table conditional monitor definitions.
401
+ */
402
+ async monitorCond(dbName, monitorId, monitorRequests) {
403
+ return await this.request("monitor_cond", [
404
+ dbName,
405
+ monitorId,
406
+ monitorRequests
407
+ ]);
408
+ }
409
+ /**
410
+ * Starts an Open vSwitch conditional monitor from a known transaction id.
411
+ *
412
+ * @param dbName Database name.
413
+ * @param monitorId Application-defined monitor id.
414
+ * @param monitorRequests Per-table conditional monitor definitions.
415
+ * @param lastTransactionId Last seen transaction id, or `null` for a fresh snapshot.
416
+ */
417
+ async monitorCondSince(dbName, monitorId, monitorRequests, lastTransactionId = null) {
418
+ return await this.request("monitor_cond_since", [
419
+ dbName,
420
+ monitorId,
421
+ monitorRequests,
422
+ lastTransactionId
423
+ ]);
424
+ }
425
+ /**
426
+ * Cancels a monitor by its monitor id.
427
+ *
428
+ * @param monitorId Monitor id used when the monitor was created.
429
+ */
430
+ async monitorCancel(monitorId) {
431
+ return await this.request("monitor_cancel", [monitorId]);
432
+ }
433
+ /**
434
+ * Acquires a named database lock.
435
+ *
436
+ * @param lockId Lock identifier.
437
+ */
438
+ async lock(lockId) {
439
+ return await this.request("lock", [lockId]);
440
+ }
441
+ /**
442
+ * Forces ownership of a named database lock.
443
+ *
444
+ * @param lockId Lock identifier.
445
+ */
446
+ async steal(lockId) {
447
+ return await this.request("steal", [lockId]);
448
+ }
449
+ /**
450
+ * Releases a previously acquired named database lock.
451
+ *
452
+ * @param lockId Lock identifier.
453
+ */
454
+ async unlock(lockId) {
455
+ return await this.request("unlock", [lockId]);
456
+ }
457
+ /**
458
+ * Sends an echo request to validate transport liveness.
459
+ *
460
+ * @param payload Values to be echoed back by the server.
461
+ */
462
+ async echo(...payload) {
463
+ return await this.request("echo", payload);
464
+ }
465
+ /**
466
+ * Enables or disables Open vSwitch database change awareness.
467
+ *
468
+ * @param enabled Whether the server should report change awareness metadata.
469
+ */
470
+ async setDbChangeAware(enabled = true) {
471
+ return await this.request("set_db_change_aware", [enabled]);
472
+ }
473
+ /**
474
+ * Closes the connection and rejects all pending requests.
475
+ */
476
+ async close() {
477
+ this.disposeTransport();
478
+ }
479
+ /**
480
+ * Implements `AsyncDisposable`.
481
+ */
482
+ async [Symbol.asyncDispose]() {
483
+ await this.close();
484
+ }
485
+ attachSocket(socket) {
486
+ this.socket = socket;
487
+ this.receiveBuffer = "";
488
+ socket.on("data", this.handleData);
489
+ socket.on("error", this.handleSocketError);
490
+ socket.on("close", this.handleSocketClose);
491
+ }
492
+ handleData = (chunk) => {
493
+ this.receiveBuffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
494
+ let frame = extractJsonFrame(this.receiveBuffer);
495
+ while (frame) {
496
+ this.receiveBuffer = frame.rest;
497
+ this.parseFrame(frame.frame);
498
+ frame = extractJsonFrame(this.receiveBuffer);
499
+ }
500
+ const trimmed = this.receiveBuffer.trimStart();
501
+ if (trimmed && trimmed[0] !== "{" && trimmed[0] !== "[") {
502
+ this.emitProtocolError("Received non-JSON data on the transport", this.receiveBuffer);
503
+ this.receiveBuffer = "";
504
+ }
505
+ };
506
+ handleSocketError = (error) => {
507
+ this.emit("transportError", error);
508
+ this.disposeTransport(error);
509
+ };
510
+ handleSocketClose = () => {
511
+ this.disposeTransport();
512
+ };
513
+ parseFrame(frame) {
514
+ try {
515
+ const payload = JSON.parse(frame);
516
+ this.handleMessage(payload);
517
+ }
518
+ catch (error) {
519
+ const protocolError = new OvsdbProtocolError("Failed to parse JSON message", frame);
520
+ this.emit("protocolError", protocolError, frame);
521
+ if (error instanceof Error) {
522
+ void error;
523
+ }
524
+ }
525
+ }
526
+ handleMessage(payload) {
527
+ if (!payload || typeof payload !== "object") {
528
+ this.emitProtocolError("Expected a JSON object message", payload);
529
+ return;
530
+ }
531
+ if ("method" in payload && typeof payload.method === "string") {
532
+ const message = payload;
533
+ if (message.id !== undefined && message.id !== null) {
534
+ void this.handleIncomingRequest(message.method, message.params ?? [], message.id);
535
+ return;
536
+ }
537
+ this.handleNotification(payload);
538
+ return;
539
+ }
540
+ if ("id" in payload) {
541
+ this.handleResponse(payload);
542
+ return;
543
+ }
544
+ this.emitProtocolError("Received message without method or id", payload);
545
+ }
546
+ handleResponse(response) {
547
+ const pendingRequest = this.pendingRequests.get(response.id);
548
+ if (!pendingRequest) {
549
+ this.emitProtocolError("Received response for an unknown request id", response);
550
+ return;
551
+ }
552
+ this.pendingRequests.delete(response.id);
553
+ clearTimeout(pendingRequest.timeoutId);
554
+ if (response.error) {
555
+ pendingRequest.reject(new OvsdbRpcError(response.error));
556
+ return;
557
+ }
558
+ pendingRequest.resolve(response.result);
559
+ }
560
+ async handleIncomingRequest(method, params, id) {
561
+ if (method === "echo") {
562
+ await this.writeMessage({
563
+ id,
564
+ result: params,
565
+ error: null
566
+ });
567
+ return;
568
+ }
569
+ await this.writeMessage({
570
+ id,
571
+ result: null,
572
+ error: {
573
+ error: "not supported",
574
+ details: `Unsupported server request: ${method}`
575
+ }
576
+ });
577
+ }
578
+ handleNotification(notification) {
579
+ this.emit("notification", notification);
580
+ switch (notification.method) {
581
+ case "update":
582
+ this.emit("update", notification);
583
+ break;
584
+ case "update2":
585
+ this.emit("update2", notification);
586
+ break;
587
+ case "update3":
588
+ this.emit("update3", notification);
589
+ break;
590
+ case "locked":
591
+ this.emit("locked", notification);
592
+ break;
593
+ case "stolen":
594
+ this.emit("stolen", notification);
595
+ break;
596
+ default:
597
+ this.emitProtocolError("Received an unknown notification method", notification);
598
+ break;
599
+ }
600
+ }
601
+ emitProtocolError(message, payload) {
602
+ const error = new OvsdbProtocolError(message, payload);
603
+ this.emit("protocolError", error, payload);
604
+ }
605
+ async writeMessage(payload) {
606
+ this.assertConnected();
607
+ await new Promise((resolve, reject) => {
608
+ this.socket?.write(`${JSON.stringify(payload)}\n`, (error) => {
609
+ if (error) {
610
+ reject(error);
611
+ return;
612
+ }
613
+ resolve();
614
+ });
615
+ });
616
+ }
617
+ assertConnected() {
618
+ if (!this.connected || !this.socket) {
619
+ throw new Error("Not connected to OVSDB");
620
+ }
621
+ }
622
+ disposeTransport(reason) {
623
+ const socket = this.socket;
624
+ this.socket = null;
625
+ this.connected = false;
626
+ this.receiveBuffer = "";
627
+ if (socket) {
628
+ socket.off("data", this.handleData);
629
+ socket.off("error", this.handleSocketError);
630
+ socket.off("close", this.handleSocketClose);
631
+ if (!socket.destroyed) {
632
+ socket.destroy();
633
+ }
634
+ }
635
+ const closeError = reason ?? new Error("Connection closed");
636
+ for (const pendingRequest of this.pendingRequests.values()) {
637
+ clearTimeout(pendingRequest.timeoutId);
638
+ pendingRequest.reject(closeError);
639
+ }
640
+ this.pendingRequests.clear();
641
+ if (!this.closeEmitted) {
642
+ this.closeEmitted = true;
643
+ this.emit("close");
644
+ }
645
+ }
646
+ }
647
+ exports.OVSDBClient = OVSDBClient;
648
+ __exportStar(require("./types"), exports);
649
+ /**
650
+ * Resolves the user-supplied transport options into an explicit connection mode.
651
+ */
652
+ function resolveConnectionOptions(options = {}) {
653
+ if (options.host) {
654
+ const port = options.port ?? 6640;
655
+ if (options.tls) {
656
+ return {
657
+ transport: "tls",
658
+ host: options.host,
659
+ port,
660
+ tlsOptions: {
661
+ host: options.host,
662
+ port,
663
+ ...options.tlsOptions
664
+ }
665
+ };
666
+ }
667
+ return {
668
+ transport: "tcp",
669
+ host: options.host,
670
+ port
671
+ };
672
+ }
673
+ return {
674
+ transport: "unix",
675
+ socketPath: options.socketPath ?? "/var/run/openvswitch/db.sock"
676
+ };
677
+ }
678
+ function createTransport(options) {
679
+ switch (options.transport) {
680
+ case "unix":
681
+ return (0, node_net_1.createConnection)(options.socketPath);
682
+ case "tcp":
683
+ return (0, node_net_1.createConnection)(options.port, options.host);
684
+ case "tls":
685
+ return (0, node_tls_1.connect)(options.tlsOptions);
686
+ }
687
+ }
688
+ function isOvsdbError(value) {
689
+ return typeof value === "object" && value !== null && "error" in value && typeof value.error === "string";
690
+ }
691
+ function extractJsonFrame(buffer) {
692
+ let startIndex = 0;
693
+ while (startIndex < buffer.length && /\s/u.test(buffer[startIndex])) {
694
+ startIndex += 1;
695
+ }
696
+ if (startIndex >= buffer.length) {
697
+ return null;
698
+ }
699
+ const opening = buffer[startIndex];
700
+ const closing = opening === "{" ? "}" : opening === "[" ? "]" : null;
701
+ if (!closing) {
702
+ return null;
703
+ }
704
+ let depth = 0;
705
+ let inString = false;
706
+ let escaping = false;
707
+ for (let index = startIndex; index < buffer.length; index += 1) {
708
+ const char = buffer[index];
709
+ if (inString) {
710
+ if (escaping) {
711
+ escaping = false;
712
+ continue;
713
+ }
714
+ if (char === "\\") {
715
+ escaping = true;
716
+ continue;
717
+ }
718
+ if (char === "\"") {
719
+ inString = false;
720
+ }
721
+ continue;
722
+ }
723
+ if (char === "\"") {
724
+ inString = true;
725
+ continue;
726
+ }
727
+ if (char === "{" || char === "[") {
728
+ depth += 1;
729
+ continue;
730
+ }
731
+ if (char === "}" || char === "]") {
732
+ depth -= 1;
733
+ if (depth === 0) {
734
+ return {
735
+ frame: buffer.slice(startIndex, index + 1),
736
+ rest: buffer.slice(index + 1)
737
+ };
738
+ }
739
+ }
740
+ }
741
+ return null;
742
+ }