@ipcom/asterisk-ari 0.0.25 → 0.0.27

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.
@@ -591,6 +591,7 @@ __export(src_exports, {
591
591
  module.exports = __toCommonJS(src_exports);
592
592
 
593
593
  // src/ari-client/ariClient.ts
594
+ var import_events4 = require("events");
594
595
  var import_exponential_backoff = __toESM(require_backoff(), 1);
595
596
 
596
597
  // src/ari-client/baseClient.ts
@@ -855,16 +856,66 @@ var Bridges = class {
855
856
  };
856
857
 
857
858
  // src/ari-client/resources/channels.ts
859
+ var import_events = require("events");
858
860
  function toQueryParams2(options) {
859
861
  return new URLSearchParams(
860
862
  Object.entries(options).filter(([, value]) => value !== void 0).map(([key, value]) => [key, value])
861
863
  // Garante que value é string
862
864
  ).toString();
863
865
  }
864
- var Channels = class {
866
+ var Channels = class extends import_events.EventEmitter {
865
867
  constructor(client) {
868
+ super();
866
869
  this.client = client;
867
870
  }
871
+ /**
872
+ * Emits a specific channel-related event.
873
+ *
874
+ * This method is called by the WebSocket client to notify about channel-related events.
875
+ * It will emit events like "ChannelDtmfReceived" to registered listeners.
876
+ *
877
+ * @param eventType - The type of WebSocket event to emit.
878
+ * @param data - The data associated with the event.
879
+ */
880
+ emitChannelEvent(eventType, data) {
881
+ if ("channel" in data) {
882
+ const channelId = data.channel?.id;
883
+ if (channelId) {
884
+ this.emit(`${eventType}:${channelId}`, data);
885
+ }
886
+ }
887
+ this.emit(eventType, data);
888
+ }
889
+ /**
890
+ * Registers a listener for a specific channel-related event.
891
+ *
892
+ * @param eventType - The type of WebSocket event to listen for.
893
+ * @param channelId - The ID of the channel to listen for.
894
+ * @param callback - The callback function to execute when the event occurs.
895
+ */
896
+ registerChannelListener(eventType, channelId, callback) {
897
+ this.on(`${eventType}:${channelId}`, callback);
898
+ }
899
+ /**
900
+ * Unregisters a listener for a specific channel-related event.
901
+ *
902
+ * @param eventType - The type of WebSocket event to stop listening for.
903
+ * @param channelId - The ID of the channel to stop listening for.
904
+ * @param callback - The callback function to remove.
905
+ */
906
+ unregisterChannelListener(eventType, channelId, callback) {
907
+ this.off(`${eventType}:${channelId}`, callback);
908
+ }
909
+ /**
910
+ * Checks if a listener is already registered for a specific channel event.
911
+ *
912
+ * @param eventType - The type of event to check.
913
+ * @param channelId - The channel ID associated with the listener.
914
+ * @returns True if a listener is already registered, false otherwise.
915
+ */
916
+ isChannelListenerRegistered(eventType, channelId) {
917
+ return this.listenerCount(`${eventType}:${channelId}`) > 0;
918
+ }
868
919
  /**
869
920
  * Lists all active channels.
870
921
  */
@@ -1201,8 +1252,10 @@ var Endpoints = class {
1201
1252
  };
1202
1253
 
1203
1254
  // src/ari-client/resources/playbacks.ts
1204
- var Playbacks = class {
1255
+ var import_events2 = require("events");
1256
+ var Playbacks = class extends import_events2.EventEmitter {
1205
1257
  constructor(client) {
1258
+ super();
1206
1259
  this.client = client;
1207
1260
  }
1208
1261
  /**
@@ -1241,6 +1294,50 @@ var Playbacks = class {
1241
1294
  async stop(playbackId) {
1242
1295
  await this.client.post(`/playbacks/${playbackId}/stop`);
1243
1296
  }
1297
+ /**
1298
+ * Registers a listener for playback events.
1299
+ * The listener is triggered for events such as "PlaybackFinished".
1300
+ *
1301
+ * @param eventType - The type of event to listen for.
1302
+ * @param playbackId - The ID of the playback to associate with this listener.
1303
+ * @param callback - The callback function to execute when the event occurs.
1304
+ */
1305
+ registerListener(eventType, playbackId, callback) {
1306
+ this.on(`${eventType}:${playbackId}`, callback);
1307
+ }
1308
+ /**
1309
+ * Unregisters a listener for playback events.
1310
+ *
1311
+ * @param eventType - The type of event to stop listening for.
1312
+ * @param playbackId - The ID of the playback associated with the listener.
1313
+ * @param callback - The callback function to remove.
1314
+ */
1315
+ unregisterListener(eventType, playbackId, callback) {
1316
+ this.off(`${eventType}:${playbackId}`, callback);
1317
+ }
1318
+ /**
1319
+ * Checks if a listener is already registered for a specific event and playback.
1320
+ *
1321
+ * @param eventType - The type of event to check.
1322
+ * @param playbackId - The playback ID associated with the listener.
1323
+ * @returns True if a listener is already registered, false otherwise.
1324
+ */
1325
+ isListenerRegistered(eventType, playbackId) {
1326
+ return this.listenerCount(`${eventType}:${playbackId}`) > 0;
1327
+ }
1328
+ /**
1329
+ * Emits playback events received via WebSocket.
1330
+ * This method should be called by the WebSocket client when playback events occur.
1331
+ *
1332
+ * @param eventType - The type of the WebSocket event.
1333
+ * @param data - The data associated with the event.
1334
+ */
1335
+ emitPlaybackEvent(eventType, data) {
1336
+ if ("playbackId" in data) {
1337
+ this.emit(`${eventType}:${data.playbackId}`, data);
1338
+ }
1339
+ this.emit(eventType, data);
1340
+ }
1244
1341
  };
1245
1342
 
1246
1343
  // src/ari-client/resources/sounds.ts
@@ -1275,16 +1372,25 @@ var Sounds = class {
1275
1372
  };
1276
1373
 
1277
1374
  // src/ari-client/websocketClient.ts
1375
+ var import_events3 = require("events");
1278
1376
  var import_ws = __toESM(require("ws"), 1);
1279
- var WebSocketClient = class {
1280
- // Para evitar reconexões paralelas
1377
+ var WebSocketClient = class extends import_events3.EventEmitter {
1378
+ /**
1379
+ * Creates a new WebSocketClient instance.
1380
+ * @param url - The WebSocket server URL to connect to.
1381
+ */
1281
1382
  constructor(url) {
1383
+ super();
1282
1384
  this.url = url;
1283
1385
  }
1284
1386
  ws = null;
1285
1387
  isClosedManually = false;
1286
- // Para evitar reconexões automáticas quando fechado manualmente
1287
1388
  isReconnecting = false;
1389
+ /**
1390
+ * Establishes a connection to the WebSocket server.
1391
+ * @returns A Promise that resolves when the connection is established, or rejects if an error occurs.
1392
+ * @throws Will throw an error if the connection fails.
1393
+ */
1288
1394
  async connect() {
1289
1395
  if (this.isReconnecting) return;
1290
1396
  return new Promise((resolve, reject) => {
@@ -1302,47 +1408,70 @@ var WebSocketClient = class {
1302
1408
  this.ws.on("close", (code, reason) => {
1303
1409
  console.warn(`WebSocket desconectado: ${code} - ${reason}`);
1304
1410
  this.isReconnecting = false;
1411
+ this.emit("close", { code, reason });
1412
+ });
1413
+ this.ws.on("message", (rawData) => {
1414
+ this.handleMessage(rawData);
1305
1415
  });
1306
1416
  });
1307
1417
  }
1308
- async reconnect() {
1309
- if (this.isClosedManually || this.isReconnecting) return;
1310
- console.log("Tentando reconectar ao WebSocket...");
1311
- this.isReconnecting = true;
1312
- try {
1313
- await this.connect();
1314
- console.log("Reconex\xE3o bem-sucedida.");
1315
- } catch (err) {
1316
- console.error("Erro ao tentar reconectar:", err);
1317
- } finally {
1318
- this.isReconnecting = false;
1319
- }
1320
- }
1418
+ /**
1419
+ * Checks if the WebSocket connection is currently open.
1420
+ * @returns True if the connection is open, false otherwise.
1421
+ */
1321
1422
  isConnected() {
1322
1423
  return this.ws?.readyState === import_ws.default.OPEN;
1323
1424
  }
1425
+ /**
1426
+ * Adds a listener for WebSocket events.
1427
+ * @param event - The event type to listen for.
1428
+ * @param callback - The function to call when the event occurs.
1429
+ * @returns The WebSocketClient instance for chaining.
1430
+ */
1324
1431
  on(event, callback) {
1325
- if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
1326
- throw new Error("WebSocket n\xE3o est\xE1 conectado.");
1327
- }
1328
- if (event === "message") {
1329
- this.ws.on(event, (rawData) => {
1330
- try {
1331
- const decodedData = JSON.parse(rawData.toString());
1332
- if (decodedData?.type) {
1333
- const matchedEvent = decodedData;
1334
- callback(matchedEvent);
1335
- } else {
1336
- console.warn("Mensagem sem tipo:", decodedData);
1337
- }
1338
- } catch (err) {
1339
- console.error("Erro ao decodificar mensagem do WebSocket:", err);
1340
- }
1341
- });
1342
- } else {
1343
- this.ws.on(event, callback);
1432
+ super.on(event, callback);
1433
+ return this;
1434
+ }
1435
+ /**
1436
+ * Removes a specific listener from a WebSocket event.
1437
+ * @param event - The event type to remove the listener from.
1438
+ * @param callback - The function to remove from the event listeners.
1439
+ * @returns The WebSocketClient instance for chaining.
1440
+ */
1441
+ off(event, callback) {
1442
+ super.off(event, callback);
1443
+ return this;
1444
+ }
1445
+ /**
1446
+ * Removes all listeners for a specific event, or all events if no event is specified.
1447
+ * @param event - Optional. The event to remove all listeners from.
1448
+ * @returns The WebSocketClient instance for chaining.
1449
+ */
1450
+ removeAllListeners(event) {
1451
+ super.removeAllListeners(event);
1452
+ return this;
1453
+ }
1454
+ /**
1455
+ * Handles incoming WebSocket messages.
1456
+ * @param rawData - The raw data received from the WebSocket.
1457
+ */
1458
+ handleMessage(rawData) {
1459
+ try {
1460
+ const decodedData = JSON.parse(rawData.toString());
1461
+ if (decodedData?.type) {
1462
+ this.emit(decodedData.type, decodedData);
1463
+ } else {
1464
+ console.warn("Mensagem recebida sem tipo:", decodedData);
1465
+ }
1466
+ } catch (err) {
1467
+ console.error("Erro ao decodificar mensagem do WebSocket:", err);
1344
1468
  }
1345
1469
  }
1470
+ /**
1471
+ * Sends data through the WebSocket connection.
1472
+ * @param data - The data to send.
1473
+ * @throws Will throw an error if the WebSocket is not connected.
1474
+ */
1346
1475
  send(data) {
1347
1476
  if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
1348
1477
  throw new Error("WebSocket n\xE3o est\xE1 conectado.");
@@ -1353,6 +1482,9 @@ var WebSocketClient = class {
1353
1482
  }
1354
1483
  });
1355
1484
  }
1485
+ /**
1486
+ * Closes the WebSocket connection manually.
1487
+ */
1356
1488
  close() {
1357
1489
  if (this.ws) {
1358
1490
  this.isClosedManually = true;
@@ -1381,6 +1513,7 @@ var AriClient = class {
1381
1513
  wsClient = null;
1382
1514
  baseClient;
1383
1515
  isReconnecting = false;
1516
+ eventEmitter = new import_events4.EventEmitter();
1384
1517
  channels;
1385
1518
  endpoints;
1386
1519
  applications;
@@ -1390,10 +1523,14 @@ var AriClient = class {
1390
1523
  bridges;
1391
1524
  /**
1392
1525
  * Connects to the ARI WebSocket for a specific application.
1526
+ * This function establishes a WebSocket connection to the Asterisk ARI, sets up event listeners,
1527
+ * and ensures the application is registered. It uses an exponential backoff strategy for connection attempts.
1393
1528
  *
1394
- * @param app - The application name to connect to.
1395
- * @param subscribedEvents
1396
- * @returns {Promise<void>} Resolves when the WebSocket connects successfully.
1529
+ * @param app - The name of the application to connect to. This is required and used to identify the application in ARI.
1530
+ * @param subscribedEvents - Optional array of WebSocketEventType to subscribe to specific events.
1531
+ * If not provided or empty, it subscribes to all events.
1532
+ * @returns A Promise that resolves when the WebSocket connection is successfully established and the application is registered.
1533
+ * @throws Error if the 'app' parameter is not provided, or if connection attempts fail after multiple retries.
1397
1534
  */
1398
1535
  async connectWebSocket(app, subscribedEvents) {
1399
1536
  if (!app) {
@@ -1421,6 +1558,11 @@ var AriClient = class {
1421
1558
  return !this.wsClient?.isConnected();
1422
1559
  }
1423
1560
  };
1561
+ if (this.wsClient?.isConnected()) {
1562
+ console.log("WebSocket j\xE1 conectado. Removendo listeners antigos...");
1563
+ this.wsClient.removeAllListeners();
1564
+ this.wsClient.close();
1565
+ }
1424
1566
  this.wsClient = new WebSocketClient(wsUrl);
1425
1567
  try {
1426
1568
  await (0, import_exponential_backoff.backOff)(async () => {
@@ -1428,6 +1570,7 @@ var AriClient = class {
1428
1570
  throw new Error("WebSocketClient instance is null.");
1429
1571
  }
1430
1572
  await this.wsClient.connect();
1573
+ this.integrateWebSocketEvents();
1431
1574
  console.log(`WebSocket conectado para o app: ${app}`);
1432
1575
  await this.ensureAppRegistered(app);
1433
1576
  }, backoffOptions);
@@ -1441,6 +1584,104 @@ var AriClient = class {
1441
1584
  this.isReconnecting = false;
1442
1585
  }
1443
1586
  }
1587
+ /**
1588
+ * Integrates WebSocket events with playback listeners.
1589
+ */
1590
+ integrateWebSocketEvents() {
1591
+ if (!this.wsClient) {
1592
+ throw new Error("WebSocket client n\xE3o est\xE1 conectado.");
1593
+ }
1594
+ const eventHandlers = {
1595
+ PlaybackFinished: (data) => {
1596
+ if ("playbackId" in data) {
1597
+ this.playbacks.emitPlaybackEvent("PlaybackFinished", data);
1598
+ }
1599
+ this.emitGlobalEvent(data);
1600
+ },
1601
+ ChannelStateChange: (data) => {
1602
+ if ("channel" in data) {
1603
+ console.log("Estado do canal alterado:", data.channel);
1604
+ }
1605
+ this.emitGlobalEvent(data);
1606
+ },
1607
+ BridgeDestroyed: (data) => {
1608
+ if ("bridge" in data) {
1609
+ console.log("Bridge destru\xEDda:", data.bridge);
1610
+ }
1611
+ this.emitGlobalEvent(data);
1612
+ },
1613
+ // Adicione mais eventos conforme necessário
1614
+ // Exemplo:
1615
+ ChannelDtmfReceived: (data) => {
1616
+ if ("channel" in data) {
1617
+ console.log("DTMF recebido no canal:", data.channel);
1618
+ this.channels.emitChannelEvent("ChannelDtmfReceived", data);
1619
+ }
1620
+ this.emitGlobalEvent(data);
1621
+ }
1622
+ };
1623
+ for (const [eventType, handler] of Object.entries(eventHandlers)) {
1624
+ if (handler) {
1625
+ this.wsClient.on(eventType, handler);
1626
+ }
1627
+ }
1628
+ console.log("Todos os eventos do WebSocket foram registrados.");
1629
+ }
1630
+ /**
1631
+ * Registra um listener para eventos globais.
1632
+ * @param callback - A função a ser executada quando um evento global for recebido.
1633
+ */
1634
+ onGlobalEvent(callback) {
1635
+ this.eventEmitter.on("globalEvent", callback);
1636
+ }
1637
+ /**
1638
+ * Remove um listener para eventos globais.
1639
+ * @param callback - A função a ser removida dos eventos globais.
1640
+ */
1641
+ offGlobalEvent(callback) {
1642
+ this.eventEmitter.off("globalEvent", callback);
1643
+ }
1644
+ /**
1645
+ * Emite um evento global.
1646
+ * @param data - Os dados do evento a serem emitidos.
1647
+ */
1648
+ emitGlobalEvent(data) {
1649
+ this.eventEmitter.emit("globalEvent", data);
1650
+ }
1651
+ /**
1652
+ * Unregisters a listener for playback events.
1653
+ *
1654
+ * This method removes a specific listener registered for a playback event type,
1655
+ * ensuring that no further notifications are sent to the callback function.
1656
+ *
1657
+ * @param eventType - The type of event to stop listening for.
1658
+ * @param playbackId - The unique ID of the playback associated with the listener.
1659
+ * @param callback - The callback function to remove from the listener.
1660
+ */
1661
+ unregisterPlaybackListener(eventType, playbackId, callback) {
1662
+ this.playbacks.unregisterListener(eventType, playbackId, callback);
1663
+ }
1664
+ /**
1665
+ * Registers a listener for playback events.
1666
+ * The listener is triggered for events such as "PlaybackFinished".
1667
+ *
1668
+ * @param eventType - The type of event to listen for.
1669
+ * @param playbackId - The ID of the playback to associate with this listener.
1670
+ * @param callback - The callback function to execute when the event occurs.
1671
+ */
1672
+ registerPlaybackListener(eventType, playbackId, callback) {
1673
+ this.playbacks.registerListener(eventType, playbackId, callback);
1674
+ }
1675
+ /**
1676
+ * Checks if a listener is already registered for a specific event and playback.
1677
+ *
1678
+ * @param eventType - The type of event to check.
1679
+ * @param playbackId - The playback ID associated with the listener.
1680
+ * @returns True if a listener is already registered, false otherwise.
1681
+ */
1682
+ isPlaybackListenerRegistered(eventType, playbackId) {
1683
+ return this.playbacks.isListenerRegistered(eventType, playbackId);
1684
+ }
1444
1685
  /**
1445
1686
  * Ensures the ARI application is registered.
1446
1687
  *
@@ -1472,23 +1713,72 @@ var AriClient = class {
1472
1713
  return this.wsClient ? this.wsClient.isConnected() : false;
1473
1714
  }
1474
1715
  /**
1475
- * Registers a callback function for WebSocket events.
1476
- * This method allows you to listen for and respond to WebSocket messages
1477
- * and process specific event types.
1716
+ * Registers a listener for WebSocket events.
1717
+ *
1718
+ * This function allows you to attach a callback function to a specific WebSocket event type.
1719
+ * The callback will be executed whenever an event of the specified type is received.
1720
+ *
1721
+ * @template T - The type of WebSocket event, extending WebSocketEvent["type"].
1722
+ * @param {T} event - The type of WebSocket event to listen for.
1723
+ * @param {(data: Extract<WebSocketEvent, { type: T }>) => void} callback - The function to be called when the event is received.
1724
+ * The callback receives the event data as its parameter.
1725
+ * @throws {Error} Throws an error if the WebSocket client is not connected when this method is called.
1726
+ * @returns {void} This function doesn't return a value.
1727
+ */
1728
+ onWebSocketEvent(event, callback) {
1729
+ if (!this.wsClient) {
1730
+ throw new Error("WebSocket n\xE3o est\xE1 conectado.");
1731
+ }
1732
+ this.wsClient.on(event, callback);
1733
+ }
1734
+ /**
1735
+ * Removes a specific listener for WebSocket events.
1736
+ *
1737
+ * This function unregisters a previously registered callback function for a specific WebSocket event type.
1738
+ * It's useful for removing event handlers when they are no longer needed or for cleaning up resources.
1739
+ *
1740
+ * @template T - The type of WebSocket event, extending WebSocketEvent["type"].
1741
+ * @param {T} event - The type of WebSocket event to remove the listener from.
1742
+ * @param {(data: Extract<WebSocketEvent, { type: T }>) => void} callback - The callback function to be removed.
1743
+ * This should be the same function reference that was used when adding the event listener.
1744
+ * @throws {Error} Throws an error if the WebSocket client is not connected when this method is called.
1745
+ * @returns {void} This function doesn't return a value.
1746
+ */
1747
+ offWebSocketEvent(event, callback) {
1748
+ if (!this.wsClient) {
1749
+ throw new Error("WebSocket n\xE3o est\xE1 conectado.");
1750
+ }
1751
+ this.wsClient.off(event, callback);
1752
+ }
1753
+ /**
1754
+ * Removes all listeners for a specific WebSocket event type.
1755
+ *
1756
+ * This function removes all event listeners that have been registered for a particular
1757
+ * WebSocket event type. It's useful for cleaning up event listeners when they are no
1758
+ * longer needed or before re-registering new listeners.
1759
+ *
1760
+ * @param event - The type of WebSocket event for which all listeners should be removed.
1761
+ * This should be a string identifying the event type (e.g., 'message', 'close', etc.).
1762
+ *
1763
+ * @throws {Error} Throws an error if the WebSocket client is not connected when this method is called.
1478
1764
  *
1479
- * @param callback - The callback function to execute when a WebSocket message is received.
1480
- * The function will receive the parsed event data as its parameter.
1481
- * @throws {Error} Throws an error if the WebSocket is not connected when trying to register the event.
1482
- * @returns {void}
1765
+ * @returns {void} This function doesn't return a value.
1483
1766
  */
1484
- onWebSocketEvent(callback) {
1767
+ removeAllWebSocketListeners(event) {
1485
1768
  if (!this.wsClient) {
1486
- throw new Error("WebSocket is not connected.");
1769
+ throw new Error("WebSocket n\xE3o est\xE1 conectado.");
1487
1770
  }
1488
- this.wsClient.on("message", callback);
1771
+ this.wsClient.removeAllListeners(event);
1489
1772
  }
1490
1773
  /**
1491
- * Closes the WebSocket connection.
1774
+ * Closes the WebSocket connection and removes all associated listeners.
1775
+ *
1776
+ * This function terminates the active WebSocket connection if one exists,
1777
+ * and cleans up by removing all event listeners attached to it. After calling
1778
+ * this function, the WebSocket client will be set to null, effectively
1779
+ * ending the connection and preparing for potential future connections.
1780
+ *
1781
+ * @returns {void} This function doesn't return a value.
1492
1782
  */
1493
1783
  closeWebSocket() {
1494
1784
  if (this.wsClient) {
@@ -1899,20 +2189,20 @@ var AriClient = class {
1899
2189
  return this.playbacks.getDetails(playbackId);
1900
2190
  }
1901
2191
  /**
1902
- * Controls a specific playback in the Asterisk server.
1903
- * This function allows manipulation of an ongoing playback, such as pausing, resuming, or skipping.
1904
- *
1905
- * @param playbackId - The unique identifier of the playback to control.
1906
- * This should be a string that uniquely identifies the playback in the Asterisk system.
1907
- * @param controlRequest - An object containing the control operation details.
1908
- * This object should conform to the PlaybackControlRequest interface,
1909
- * which includes an 'operation' property specifying the control action to perform.
1910
- * @returns A Promise that resolves when the control operation is successfully executed.
1911
- * The promise resolves to void, indicating no specific return value.
1912
- * If an error occurs during the operation, the promise will be rejected with an error object.
1913
- * @throws Will throw an error if the playback control operation fails, e.g., if the playback doesn't exist
1914
- * or the requested operation is invalid.
1915
- */
2192
+ * Controls a specific playback in the Asterisk server.
2193
+ * This function allows manipulation of an ongoing playback, such as pausing, resuming, or skipping.
2194
+ *
2195
+ * @param playbackId - The unique identifier of the playback to control.
2196
+ * This should be a string that uniquely identifies the playback in the Asterisk system.
2197
+ * @param controlRequest - An object containing the control operation details.
2198
+ * This object should conform to the PlaybackControlRequest interface,
2199
+ * which includes an 'operation' property specifying the control action to perform.
2200
+ * @returns A Promise that resolves when the control operation is successfully executed.
2201
+ * The promise resolves to void, indicating no specific return value.
2202
+ * If an error occurs during the operation, the promise will be rejected with an error object.
2203
+ * @throws Will throw an error if the playback control operation fails, e.g., if the playback doesn't exist
2204
+ * or the requested operation is invalid.
2205
+ */
1916
2206
  async controlPlayback(playbackId, controlRequest) {
1917
2207
  const { operation } = controlRequest;
1918
2208
  return this.playbacks.control(playbackId, operation);