55/* c8 ignore start */
66
77const {
8+ ArrayBufferPrototypeGetByteLength,
89 ArrayBufferPrototypeTransfer,
910 ArrayIsArray,
1011 ArrayPrototypePush,
1112 BigInt,
13+ DataViewPrototypeGetBuffer,
14+ DataViewPrototypeGetByteLength,
15+ DataViewPrototypeGetByteOffset,
1216 ObjectDefineProperties,
1317 ObjectKeys,
1418 PromiseWithResolvers,
1519 SafeSet,
1620 SymbolAsyncDispose,
21+ TypedArrayPrototypeGetBuffer,
22+ TypedArrayPrototypeGetByteLength,
23+ TypedArrayPrototypeGetByteOffset,
24+ TypedArrayPrototypeSlice,
1725 Uint8Array,
1826} = primordials ;
1927
@@ -66,6 +74,8 @@ const {
6674const {
6775 isArrayBuffer,
6876 isArrayBufferView,
77+ isDataView,
78+ isPromise,
6979 isSharedArrayBuffer,
7080} = require ( 'util/types' ) ;
7181
@@ -190,6 +200,8 @@ const onSessionVersionNegotiationChannel = dc.channel('quic.session.version.nego
190200const onSessionOriginChannel = dc . channel ( 'quic.session.receive.origin' ) ;
191201const onSessionHandshakeChannel = dc . channel ( 'quic.session.handshake' ) ;
192202
203+ const kNilDatagramId = 0n ;
204+
193205/**
194206 * @typedef {import('../socketaddress.js').SocketAddress } SocketAddress
195207 * @typedef {import('../crypto/keys.js').KeyObject } KeyObject
@@ -427,7 +439,7 @@ setCallbacks({
427439 * @param {boolean } early
428440 */
429441 onSessionDatagram ( uint8Array , early ) {
430- debug ( 'session datagram callback' , uint8Array . byteLength , early ) ;
442+ debug ( 'session datagram callback' , TypedArrayPrototypeGetByteLength ( uint8Array ) , early ) ;
431443 this [ kOwner ] [ kDatagram ] ( uint8Array , early ) ;
432444 } ,
433445
@@ -1100,6 +1112,8 @@ class QuicSession {
11001112 #onstream = undefined ;
11011113 /** @type {OnDatagramCallback|undefined } */
11021114 #ondatagram = undefined ;
1115+ /** @type {OnDatagramStatusCallback|undefined } */
1116+ #ondatagramstatus = undefined ;
11031117 /** @type {object|undefined } */
11041118 #sessionticket = undefined ;
11051119 /** @type {object|undefined } */
@@ -1200,6 +1214,38 @@ class QuicSession {
12001214 }
12011215 }
12021216
1217+ /**
1218+ * The ondatagramstatus callback is called when the status of a sent datagram
1219+ * is received. This is best-effort only.
1220+ * @type {OnDatagramStatusCallback }
1221+ */
1222+ get ondatagramstatus ( ) {
1223+ QuicSession . #assertIsQuicSession( this ) ;
1224+ return this . #ondatagramstatus;
1225+ }
1226+
1227+ set ondatagramstatus ( fn ) {
1228+ QuicSession . #assertIsQuicSession( this ) ;
1229+ if ( fn === undefined ) {
1230+ this . #ondatagramstatus = undefined ;
1231+ this . #state. hasDatagramStatusListener = false ;
1232+ } else {
1233+ validateFunction ( fn , 'ondatagramstatus' ) ;
1234+ this . #ondatagramstatus = fn . bind ( this ) ;
1235+ this . #state. hasDatagramStatusListener = true ;
1236+ }
1237+ }
1238+
1239+ /**
1240+ * The maximum datagram size the peer will accept, or 0 if datagrams
1241+ * are not supported or the handshake has not yet completed.
1242+ * @type {bigint }
1243+ */
1244+ get maxDatagramSize ( ) {
1245+ QuicSession . #assertIsQuicSession( this ) ;
1246+ return this . #state. maxDatagramSize ;
1247+ }
1248+
12031249 /**
12041250 * The statistics collected for this session.
12051251 * @type {QuicSessionStats }
@@ -1312,42 +1358,93 @@ class QuicSession {
13121358 * of the sent datagram will be reported via the datagram-status event if
13131359 * possible.
13141360 *
1315- * If a string is given it will be encoded as UTF-8 .
1361+ * If a string is given it will be encoded using the specified encoding .
13161362 *
1317- * If an ArrayBufferView is given, the view will be copied.
1318- * @param {ArrayBufferView|string } datagram The datagram payload
1319- * @returns {Promise<void> }
1363+ * If an ArrayBufferView is given, the underlying ArrayBuffer will be
1364+ * transferred if possible, otherwise the data will be copied.
1365+ *
1366+ * If a Promise is given, it will be awaited before sending. If the
1367+ * session closes while awaiting, 0n is returned silently.
1368+ * @param {ArrayBufferView|string|Promise } datagram The datagram payload
1369+ * @param {string } [encoding] The encoding to use if datagram is a string
1370+ * @returns {Promise<bigint> } The datagram ID
13201371 */
1321- async sendDatagram ( datagram ) {
1372+ async sendDatagram ( datagram , encoding = 'utf8' ) {
13221373 QuicSession . #assertIsQuicSession( this ) ;
13231374 if ( this . #isClosedOrClosing) {
13241375 throw new ERR_INVALID_STATE ( 'Session is closed' ) ;
13251376 }
1377+ let offset , length , buffer ;
1378+
1379+ const maxDatagramSize = this . #state. maxDatagramSize ;
1380+
1381+ // The peer max datagram size is either unknown or they have explicitly
1382+ // indicated that they do not support datagrams by setting it to 0. In
1383+ // either case, we do not send the datagram.
1384+ if ( maxDatagramSize === 0n ) return kNilDatagramId ;
1385+
1386+ if ( isPromise ( datagram ) ) {
1387+ datagram = await datagram ;
1388+ // Session may have closed while awaiting. Since datagrams are
1389+ // inherently unreliable, silently return rather than throwing.
1390+ if ( this . #isClosedOrClosing) return kNilDatagramId ;
1391+ }
1392+
13261393 if ( typeof datagram === 'string' ) {
1327- datagram = Buffer . from ( datagram , 'utf8' ) ;
1394+ datagram = Buffer . from ( datagram , encoding ) ;
1395+ length = TypedArrayPrototypeGetByteLength ( datagram ) ;
1396+ if ( length === 0 ) return kNilDatagramId ;
13281397 } else {
13291398 if ( ! isArrayBufferView ( datagram ) ) {
13301399 throw new ERR_INVALID_ARG_TYPE ( 'datagram' ,
13311400 [ 'ArrayBufferView' , 'string' ] ,
13321401 datagram ) ;
13331402 }
1334- const length = datagram . byteLength ;
1335- const offset = datagram . byteOffset ;
1336- datagram = new Uint8Array ( ArrayBufferPrototypeTransfer ( datagram . buffer ) ,
1337- length , offset ) ;
1403+ if ( isDataView ( datagram ) ) {
1404+ offset = DataViewPrototypeGetByteOffset ( datagram ) ;
1405+ length = DataViewPrototypeGetByteLength ( datagram ) ;
1406+ buffer = DataViewPrototypeGetBuffer ( datagram ) ;
1407+ } else {
1408+ offset = TypedArrayPrototypeGetByteOffset ( datagram ) ;
1409+ length = TypedArrayPrototypeGetByteLength ( datagram ) ;
1410+ buffer = TypedArrayPrototypeGetBuffer ( datagram ) ;
1411+ }
1412+
1413+ // If the view has zero length (e.g. detached buffer), there's
1414+ // nothing to send.
1415+ if ( length === 0 ) return kNilDatagramId ;
1416+
1417+ if ( isSharedArrayBuffer ( buffer ) ||
1418+ offset !== 0 ||
1419+ length !== ArrayBufferPrototypeGetByteLength ( buffer ) ) {
1420+ // Copy if the buffer is not transferable (SharedArrayBuffer)
1421+ // or if the view is over a subset of the buffer (e.g. a
1422+ // Node.js Buffer from the pool).
1423+ datagram = TypedArrayPrototypeSlice (
1424+ new Uint8Array ( buffer ) , offset , offset + length ) ;
1425+ } else {
1426+ datagram = new Uint8Array (
1427+ ArrayBufferPrototypeTransfer ( buffer ) , offset , length ) ;
1428+ }
13381429 }
13391430
1340- debug ( `sending datagram with ${ datagram . byteLength } bytes` ) ;
1431+ // The peer max datagram size is less than the datagram we want to send,
1432+ // so... don't send it.
1433+ if ( length > maxDatagramSize ) return kNilDatagramId ;
13411434
13421435 const id = this . #handle. sendDatagram ( datagram ) ;
13431436
1344- if ( onSessionSendDatagramChannel . hasSubscribers ) {
1437+ if ( id !== kNilDatagramId && onSessionSendDatagramChannel . hasSubscribers ) {
13451438 onSessionSendDatagramChannel . publish ( {
1439+ __proto__ : null ,
13461440 id,
1347- length : datagram . byteLength ,
1441+ length,
13481442 session : this ,
13491443 } ) ;
13501444 }
1445+
1446+ debug ( `datagram ${ id } sent with ${ length } bytes` ) ;
1447+ return id ;
13511448 }
13521449
13531450 /**
@@ -1472,6 +1569,7 @@ class QuicSession {
14721569
14731570 this . #onstream = undefined ;
14741571 this . #ondatagram = undefined ;
1572+ this . #ondatagramstatus = undefined ;
14751573 this . #sessionticket = undefined ;
14761574 this . #token = undefined ;
14771575
@@ -1531,19 +1629,20 @@ class QuicSession {
15311629 }
15321630
15331631 /**
1534- * @param {Uint8Array } u8
1535- * @param {boolean } early
1632+ * @param {Uint8Array } u8 The datagram payload
1633+ * @param {boolean } early A boolean indicating whether this datagram was received before the handshake completed
15361634 */
15371635 [ kDatagram ] ( u8 , early ) {
1538- // The datagram event should only be called if the session was created with
1636+ // The datagram event should only be called if the session has
15391637 // an ondatagram callback. The callback should always exist here.
1540- assert ( this . #ondatagram, 'Unexpected datagram event' ) ;
1638+ assert ( typeof this . #ondatagram === 'function' , 'Unexpected datagram event' ) ;
15411639 if ( this . destroyed ) return ;
1542- const length = u8 . byteLength ;
1640+ const length = TypedArrayPrototypeGetByteLength ( u8 ) ;
15431641 this . #ondatagram( u8 , early ) ;
15441642
15451643 if ( onSessionReceiveDatagramChannel . hasSubscribers ) {
15461644 onSessionReceiveDatagramChannel . publish ( {
1645+ __proto__ : null ,
15471646 length,
15481647 early,
15491648 session : this ,
@@ -1556,9 +1655,15 @@ class QuicSession {
15561655 * @param {'lost'|'acknowledged' } status
15571656 */
15581657 [ kDatagramStatus ] ( id , status ) {
1658+ // The datagram status event should only be called if the session has
1659+ // an ondatagramstatus callback. The callback should always exist here.
1660+ assert ( typeof this . #ondatagramstatus === 'function' , 'Unexpected datagram status event' ) ;
15591661 if ( this . destroyed ) return ;
1662+ this . #ondatagramstatus( id , status ) ;
1663+
15601664 if ( onSessionReceiveDatagramStatusChannel . hasSubscribers ) {
15611665 onSessionReceiveDatagramStatusChannel . publish ( {
1666+ __proto__ : null ,
15621667 id,
15631668 status,
15641669 session : this ,
0 commit comments