@enbox/dwn-clients 0.4.4 → 0.4.6
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.
- package/dist/esm/dwn-rpc-error.js +45 -0
- package/dist/esm/dwn-rpc-error.js.map +1 -0
- package/dist/esm/http-dwn-rpc-client.js +109 -50
- package/dist/esm/http-dwn-rpc-client.js.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/json-rpc-socket.js +5 -5
- package/dist/esm/json-rpc-socket.js.map +1 -1
- package/dist/esm/readable-stream.js +68 -0
- package/dist/esm/readable-stream.js.map +1 -0
- package/dist/esm/replication-apply-result.js +105 -0
- package/dist/esm/replication-apply-result.js.map +1 -0
- package/dist/esm/rpc-client.js +10 -5
- package/dist/esm/rpc-client.js.map +1 -1
- package/dist/esm/web-socket-clients.js +152 -17
- package/dist/esm/web-socket-clients.js.map +1 -1
- package/dist/esm/ws-payload-size.js +12 -0
- package/dist/esm/ws-payload-size.js.map +1 -0
- package/dist/types/dwn-rpc-error.d.ts +18 -0
- package/dist/types/dwn-rpc-error.d.ts.map +1 -0
- package/dist/types/dwn-rpc-types.d.ts +37 -1
- package/dist/types/dwn-rpc-types.d.ts.map +1 -1
- package/dist/types/http-dwn-rpc-client.d.ts +3 -3
- package/dist/types/http-dwn-rpc-client.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/json-rpc-socket.d.ts.map +1 -1
- package/dist/types/readable-stream.d.ts +6 -0
- package/dist/types/readable-stream.d.ts.map +1 -0
- package/dist/types/replication-apply-result.d.ts +3 -0
- package/dist/types/replication-apply-result.d.ts.map +1 -0
- package/dist/types/rpc-client.d.ts +3 -2
- package/dist/types/rpc-client.d.ts.map +1 -1
- package/dist/types/web-socket-clients.d.ts +14 -1
- package/dist/types/web-socket-clients.d.ts.map +1 -1
- package/dist/types/ws-payload-size.d.ts +6 -0
- package/dist/types/ws-payload-size.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/dwn-rpc-error.ts +52 -0
- package/src/dwn-rpc-types.ts +52 -1
- package/src/http-dwn-rpc-client.ts +132 -58
- package/src/index.ts +3 -0
- package/src/json-rpc-socket.ts +5 -6
- package/src/readable-stream.ts +59 -0
- package/src/replication-apply-result.ts +124 -0
- package/src/rpc-client.ts +16 -5
- package/src/web-socket-clients.ts +210 -19
- package/src/ws-payload-size.ts +14 -0
package/src/rpc-client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JsonRpcResponse } from './json-rpc.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ReplicationApplyResult } from '@enbox/dwn-sdk-js';
|
|
3
|
+
import type { DwnReplicationApplyRequest, DwnRpc, DwnRpcRequest, DwnRpcResponse } from './dwn-rpc-types.js';
|
|
3
4
|
import type { DwnServerInfoRpc, ServerInfo } from './server-info-types.js';
|
|
4
5
|
|
|
5
6
|
import { createJsonRpcRequest } from './json-rpc.js';
|
|
@@ -94,6 +95,20 @@ export class EnboxRpcClient implements EnboxRpc {
|
|
|
94
95
|
return transportClient.sendDwnRequest(request);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
applyReplicatedMessage(request: DwnReplicationApplyRequest): Promise<ReplicationApplyResult> {
|
|
99
|
+
const url = new URL(request.dwnUrl);
|
|
100
|
+
|
|
101
|
+
const transportClient = this.transportClients.get(url.protocol);
|
|
102
|
+
if (!transportClient) {
|
|
103
|
+
const error = new Error(`no ${url.protocol} transport client available`);
|
|
104
|
+
error.name = 'NO_TRANSPORT_CLIENT';
|
|
105
|
+
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return transportClient.applyReplicatedMessage(request);
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
async getServerInfo(dwnUrl: string): Promise<ServerInfo> {
|
|
98
113
|
// will throw if url is invalid
|
|
99
114
|
const url = new URL(dwnUrl);
|
|
@@ -153,8 +168,4 @@ export class WebSocketEnboxRpcClient extends WebSocketDwnRpcClient implements En
|
|
|
153
168
|
async sendDidRequest(_request: DidRpcRequest): Promise<DidRpcResponse> {
|
|
154
169
|
throw new Error(`not implemented for transports [${this.transportProtocols.join(', ')}]`);
|
|
155
170
|
}
|
|
156
|
-
|
|
157
|
-
async getServerInfo(_dwnUrl: string): Promise<ServerInfo> {
|
|
158
|
-
throw new Error(`not implemented for transports [${this.transportProtocols.join(', ')}]`);
|
|
159
|
-
}
|
|
160
171
|
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
DwnReplicationApplyRequest,
|
|
3
|
+
DwnRpc,
|
|
4
|
+
DwnRpcRequest,
|
|
5
|
+
DwnRpcResponse,
|
|
6
|
+
DwnSubscriptionHandler,
|
|
7
|
+
ResubscribeFactory,
|
|
8
|
+
} from './dwn-rpc-types.js';
|
|
9
|
+
import type { DwnServerInfoRpc, ServerInfo } from './server-info-types.js';
|
|
10
|
+
import type {
|
|
11
|
+
GenericMessage,
|
|
12
|
+
MessageSubscription,
|
|
13
|
+
ProgressToken,
|
|
14
|
+
ReplicationApplyResult,
|
|
15
|
+
SubscriptionMessage,
|
|
16
|
+
UnionMessageReply,
|
|
17
|
+
} from '@enbox/dwn-sdk-js';
|
|
3
18
|
|
|
4
19
|
import { CryptoUtils } from '@enbox/crypto';
|
|
20
|
+
import { DwnRpcError } from './dwn-rpc-error.js';
|
|
21
|
+
import { HttpDwnRpcClient } from './http-dwn-rpc-client.js';
|
|
5
22
|
import { JsonRpcSocket } from './json-rpc-socket.js';
|
|
6
|
-
import {
|
|
23
|
+
import { parseReplicationApplyResult } from './replication-apply-result.js';
|
|
24
|
+
import { createJsonRpcAck, createJsonRpcRequest, createJsonRpcSubscriptionRequest, JsonRpcErrorCodes } from './json-rpc.js';
|
|
25
|
+
import { DataStream, Encoder } from '@enbox/dwn-sdk-js';
|
|
26
|
+
import { DEFAULT_MAX_WS_RAW_RECORD_DATA_BYTES, maxWsJsonRpcPayloadBytes } from './ws-payload-size.js';
|
|
27
|
+
|
|
28
|
+
const DEFAULT_MAX_WS_JSON_RPC_PAYLOAD_BYTES = maxWsJsonRpcPayloadBytes(DEFAULT_MAX_WS_RAW_RECORD_DATA_BYTES);
|
|
7
29
|
|
|
8
30
|
/**
|
|
9
31
|
* Metadata for a tracked subscription, including everything needed to
|
|
@@ -39,6 +61,14 @@ interface SocketConnection {
|
|
|
39
61
|
url: string;
|
|
40
62
|
}
|
|
41
63
|
|
|
64
|
+
function connectionCacheKey(url: URL): string {
|
|
65
|
+
return stripTrailingSlash(url.toString());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stripTrailingSlash(url: string): string {
|
|
69
|
+
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
function shouldReplaceLastCursor(current: ProgressToken | undefined, candidate: ProgressToken): boolean {
|
|
43
73
|
if (current === undefined) {
|
|
44
74
|
return true;
|
|
@@ -53,8 +83,11 @@ function shouldReplaceLastCursor(current: ProgressToken | undefined, candidate:
|
|
|
53
83
|
|
|
54
84
|
export class WebSocketDwnRpcClient implements DwnRpc {
|
|
55
85
|
public get transportProtocols(): string[] { return ['ws:', 'wss:']; }
|
|
56
|
-
// a map of
|
|
86
|
+
// a map of normalized DWN WebSocket endpoint URLs to WebSocket connections
|
|
57
87
|
private static readonly connections = new Map<string, SocketConnection>();
|
|
88
|
+
private static readonly pendingConnections = new Map<string, Promise<SocketConnection>>();
|
|
89
|
+
|
|
90
|
+
public constructor(private readonly serverInfoRpc: DwnServerInfoRpc = new HttpDwnRpcClient()) {}
|
|
58
91
|
|
|
59
92
|
async sendDwnRequest(request: DwnRpcRequest): Promise<DwnRpcResponse> {
|
|
60
93
|
|
|
@@ -64,18 +97,7 @@ export class WebSocketDwnRpcClient implements DwnRpc {
|
|
|
64
97
|
throw new Error(`Invalid websocket protocol ${url.protocol}`);
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
const hasConnection = WebSocketDwnRpcClient.connections.has(url.host);
|
|
69
|
-
if (!hasConnection) {
|
|
70
|
-
try {
|
|
71
|
-
const connection = await WebSocketDwnRpcClient.createConnection(url);
|
|
72
|
-
WebSocketDwnRpcClient.connections.set(url.host, connection);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
throw new Error(`Error connecting to ${url.host}: ${(error as Error).message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const connection = WebSocketDwnRpcClient.connections.get(url.host)!;
|
|
100
|
+
const connection = await WebSocketDwnRpcClient.getConnection(request.dwnUrl);
|
|
79
101
|
const { targetDid, message, subscription } = request;
|
|
80
102
|
|
|
81
103
|
if (subscription) {
|
|
@@ -87,17 +109,71 @@ export class WebSocketDwnRpcClient implements DwnRpc {
|
|
|
87
109
|
return WebSocketDwnRpcClient.processMessage(connection, targetDid, message);
|
|
88
110
|
}
|
|
89
111
|
|
|
112
|
+
async applyReplicatedMessage(request: DwnReplicationApplyRequest): Promise<ReplicationApplyResult> {
|
|
113
|
+
WebSocketDwnRpcClient.assertReplicatedApplyDataIsPresent(request);
|
|
114
|
+
const maxPayloadBytes = await this.maxPayloadBytesForReplicatedApply(request);
|
|
115
|
+
WebSocketDwnRpcClient.assertReplicatedApplyDataSizeIsSupported(request, maxPayloadBytes);
|
|
116
|
+
const connection = await WebSocketDwnRpcClient.getConnection(request.dwnUrl);
|
|
117
|
+
const encodedData = request.data === undefined ? undefined : await dataToBase64Url(request.data);
|
|
118
|
+
return WebSocketDwnRpcClient.applyReplicatedMessage(connection, request.targetDid, request.message, encodedData, maxPayloadBytes);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getServerInfo(dwnUrl: string): Promise<ServerInfo> {
|
|
122
|
+
return this.serverInfoRpc.getServerInfo(httpUrlForWsDwnUrl(dwnUrl));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async maxPayloadBytesForReplicatedApply(request: DwnReplicationApplyRequest): Promise<number> {
|
|
126
|
+
const dataSize = recordsWriteDataSize(request.message);
|
|
127
|
+
if (dataSize === undefined) {
|
|
128
|
+
return DEFAULT_MAX_WS_JSON_RPC_PAYLOAD_BYTES;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const serverInfo = await this.getServerInfo(request.dwnUrl);
|
|
132
|
+
return maxWsJsonRpcPayloadBytes(serverInfo.maxFileSize);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static async getConnection(dwnUrl: string): Promise<SocketConnection> {
|
|
136
|
+
const url = new URL(dwnUrl);
|
|
137
|
+
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
|
|
138
|
+
throw new Error(`Invalid websocket protocol ${url.protocol}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const key = connectionCacheKey(url);
|
|
142
|
+
const existing = WebSocketDwnRpcClient.connections.get(key);
|
|
143
|
+
if (existing !== undefined) {
|
|
144
|
+
return existing;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let pending = WebSocketDwnRpcClient.pendingConnections.get(key);
|
|
148
|
+
if (pending === undefined) {
|
|
149
|
+
pending = WebSocketDwnRpcClient.createConnection(url)
|
|
150
|
+
.then((connection) => {
|
|
151
|
+
WebSocketDwnRpcClient.connections.set(key, connection);
|
|
152
|
+
return connection;
|
|
153
|
+
})
|
|
154
|
+
.catch((error: unknown) => {
|
|
155
|
+
throw new Error(`Error connecting to ${key}: ${(error as Error).message}`);
|
|
156
|
+
})
|
|
157
|
+
.finally(() => {
|
|
158
|
+
WebSocketDwnRpcClient.pendingConnections.delete(key);
|
|
159
|
+
});
|
|
160
|
+
WebSocketDwnRpcClient.pendingConnections.set(key, pending);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return pending;
|
|
164
|
+
}
|
|
165
|
+
|
|
90
166
|
/**
|
|
91
167
|
* Creates a new `SocketConnection` with lifecycle wiring for reconnection.
|
|
92
168
|
*/
|
|
93
169
|
private static async createConnection(url: URL): Promise<SocketConnection> {
|
|
94
|
-
const
|
|
170
|
+
const key = connectionCacheKey(url);
|
|
95
171
|
const subscriptions = new Map<string, TrackedSubscription>();
|
|
96
172
|
|
|
97
173
|
const socket = await JsonRpcSocket.connect(url.toString(), {
|
|
98
174
|
onclose: (): void => {
|
|
99
175
|
// Remove the stale connection from the map so new requests create a fresh one.
|
|
100
|
-
WebSocketDwnRpcClient.connections.delete(
|
|
176
|
+
WebSocketDwnRpcClient.connections.delete(key);
|
|
101
177
|
|
|
102
178
|
// Notify all subscription handlers of disconnection.
|
|
103
179
|
for (const tracked of subscriptions.values()) {
|
|
@@ -114,7 +190,7 @@ export class WebSocketDwnRpcClient implements DwnRpc {
|
|
|
114
190
|
onreconnected: (): void => {
|
|
115
191
|
// Re-register this connection in the map (it was deleted on close).
|
|
116
192
|
const conn = { socket, subscriptions, url: url.toString() };
|
|
117
|
-
WebSocketDwnRpcClient.connections.set(
|
|
193
|
+
WebSocketDwnRpcClient.connections.set(key, conn);
|
|
118
194
|
|
|
119
195
|
// Resubscribe all tracked subscriptions with their last known cursor.
|
|
120
196
|
WebSocketDwnRpcClient.resubscribeAll(conn);
|
|
@@ -141,6 +217,32 @@ export class WebSocketDwnRpcClient implements DwnRpc {
|
|
|
141
217
|
return result.reply as DwnRpcResponse;
|
|
142
218
|
}
|
|
143
219
|
|
|
220
|
+
private static async applyReplicatedMessage(
|
|
221
|
+
connection: SocketConnection,
|
|
222
|
+
target: string,
|
|
223
|
+
message: DwnReplicationApplyRequest['message'],
|
|
224
|
+
encodedData?: string,
|
|
225
|
+
maxPayloadBytes: number = DEFAULT_MAX_WS_JSON_RPC_PAYLOAD_BYTES,
|
|
226
|
+
): Promise<ReplicationApplyResult> {
|
|
227
|
+
const requestId = CryptoUtils.randomUuid();
|
|
228
|
+
const request = createJsonRpcRequest(requestId, 'dwn.applyReplicatedMessage', {
|
|
229
|
+
target,
|
|
230
|
+
message,
|
|
231
|
+
...(encodedData === undefined ? {} : { encodedData }),
|
|
232
|
+
});
|
|
233
|
+
WebSocketDwnRpcClient.assertPayloadFitsFrame(request, encodedData, maxPayloadBytes);
|
|
234
|
+
|
|
235
|
+
const { socket } = connection;
|
|
236
|
+
const response = await socket.request(request);
|
|
237
|
+
|
|
238
|
+
const { error, result } = response;
|
|
239
|
+
if (error !== undefined) {
|
|
240
|
+
throw new DwnRpcError(error.code, error.message, error.data);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return parseReplicationApplyResult(result.result);
|
|
244
|
+
}
|
|
245
|
+
|
|
144
246
|
private static async subscriptionRequest(
|
|
145
247
|
connection: SocketConnection,
|
|
146
248
|
target: string,
|
|
@@ -273,4 +375,93 @@ export class WebSocketDwnRpcClient implements DwnRpc {
|
|
|
273
375
|
}
|
|
274
376
|
}
|
|
275
377
|
}
|
|
378
|
+
|
|
379
|
+
private static assertReplicatedApplyDataIsPresent(request: DwnReplicationApplyRequest): void {
|
|
380
|
+
const dataSize = recordsWriteDataSize(request.message);
|
|
381
|
+
if (dataSize !== undefined && dataSize > 0 && request.data === undefined) {
|
|
382
|
+
throw new DwnRpcError(
|
|
383
|
+
JsonRpcErrorCodes.InvalidParams,
|
|
384
|
+
'data-bearing RecordsWrite replicated apply over WebSocket requires encoded data',
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private static assertReplicatedApplyDataSizeIsSupported(request: DwnReplicationApplyRequest, maxPayloadBytes: number): void {
|
|
390
|
+
const dataSize = recordsWriteDataSize(request.message);
|
|
391
|
+
if (dataSize !== undefined && maxWsJsonRpcPayloadBytes(dataSize) > maxPayloadBytes) {
|
|
392
|
+
throw new DwnRpcError(
|
|
393
|
+
JsonRpcErrorCodes.InvalidParams,
|
|
394
|
+
`RecordsWrite replicated apply data is too large for WebSocket JSON-RPC framing`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private static assertPayloadFitsFrame(
|
|
400
|
+
request: ReturnType<typeof createJsonRpcRequest>,
|
|
401
|
+
encodedData: string | undefined,
|
|
402
|
+
maxPayloadBytes: number,
|
|
403
|
+
): void {
|
|
404
|
+
const payloadBytes = estimatedJsonRpcPayloadBytes(request, encodedData);
|
|
405
|
+
if (payloadBytes > maxPayloadBytes) {
|
|
406
|
+
throw new DwnRpcError(
|
|
407
|
+
JsonRpcErrorCodes.InvalidParams,
|
|
408
|
+
`replicated apply JSON-RPC payload is too large for WebSocket transport`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function httpUrlForWsDwnUrl(dwnUrl: string): string {
|
|
415
|
+
const url = new URL(dwnUrl);
|
|
416
|
+
if (url.protocol === 'ws:') {
|
|
417
|
+
url.protocol = 'http:';
|
|
418
|
+
} else if (url.protocol === 'wss:') {
|
|
419
|
+
url.protocol = 'https:';
|
|
420
|
+
}
|
|
421
|
+
return url.toString();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function estimatedJsonRpcPayloadBytes(request: ReturnType<typeof createJsonRpcRequest>, encodedData: string | undefined): number {
|
|
425
|
+
if (encodedData === undefined) {
|
|
426
|
+
return utf8ByteLength(JSON.stringify(request));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const params = request.params as Record<string, unknown>;
|
|
430
|
+
const requestWithoutData = {
|
|
431
|
+
...request,
|
|
432
|
+
params: {
|
|
433
|
+
...params,
|
|
434
|
+
encodedData: '',
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
return utf8ByteLength(JSON.stringify(requestWithoutData)) + encodedData.length;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function utf8ByteLength(text: string): number {
|
|
441
|
+
return new TextEncoder().encode(text).byteLength;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function dataToBase64Url(data: DwnReplicationApplyRequest['data']): Promise<string> {
|
|
445
|
+
if (data instanceof Blob) {
|
|
446
|
+
return Encoder.bytesToBase64Url(new Uint8Array(await data.arrayBuffer()));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (data instanceof ReadableStream) {
|
|
450
|
+
return Encoder.bytesToBase64Url(await DataStream.toBytes(data as ReadableStream<Uint8Array>));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (data instanceof Uint8Array) {
|
|
454
|
+
return Encoder.bytesToBase64Url(data);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return Encoder.bytesToBase64Url(new Uint8Array(await new Blob([data] as BlobPart[]).arrayBuffer()));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function recordsWriteDataSize(message: DwnReplicationApplyRequest['message']): number | undefined {
|
|
461
|
+
const descriptor = (message as { descriptor?: { interface?: unknown; method?: unknown; dataSize?: unknown } }).descriptor;
|
|
462
|
+
return descriptor?.interface === 'Records' &&
|
|
463
|
+
descriptor.method === 'Write' &&
|
|
464
|
+
typeof descriptor.dataSize === 'number'
|
|
465
|
+
? descriptor.dataSize
|
|
466
|
+
: undefined;
|
|
276
467
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DEFAULT_MAX_WS_RAW_RECORD_DATA_BYTES = 100 * 1024 * 1024;
|
|
2
|
+
export const WS_JSON_RPC_ENVELOPE_BYTES = 64 * 1024;
|
|
3
|
+
|
|
4
|
+
export function base64UrlEncodedLength(byteLength: number): number {
|
|
5
|
+
return Math.ceil(byteLength / 3) * 4;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function estimatedWsJsonRpcPayloadBytes(rawDataSize: number): number {
|
|
9
|
+
return base64UrlEncodedLength(rawDataSize) + WS_JSON_RPC_ENVELOPE_BYTES;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function maxWsJsonRpcPayloadBytes(maxRecordDataSize: number): number {
|
|
13
|
+
return estimatedWsJsonRpcPayloadBytes(maxRecordDataSize);
|
|
14
|
+
}
|