@react-native-windows/automation-channel 0.85.0-preview.1 → 0.85.0-preview.2

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.
@@ -26,6 +26,9 @@ export declare class AutomationClient {
26
26
  private onEnd;
27
27
  private onError;
28
28
  private onMessage;
29
+ private rejectPendingRequest;
30
+ private failUnexpectedResponseId;
31
+ private failAllPendingRequests;
29
32
  }
30
33
  export declare function waitForConnection(opts: {
31
34
  port: number;
@@ -50,19 +50,26 @@ class AutomationClient {
50
50
  }
51
51
  onData(chunk) {
52
52
  this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
53
- if (this.receiveBuffer.length >= 4) {
53
+ // Localhost TCP can coalesce responses into one chunk, so drain every
54
+ // complete frame and advance past it; stopping after one leaves later
55
+ // responses stuck behind it and wedges the channel.
56
+ while (this.receiveBuffer.length >= 4) {
54
57
  const messageLength = this.receiveBuffer.readUInt32LE();
55
58
  const totalLength = messageLength + 4;
56
- if (this.receiveBuffer.length >= totalLength) {
57
- const messageBuffer = this.receiveBuffer.slice(4, totalLength);
58
- if (totalLength < this.receiveBuffer.length) {
59
- this.receiveBuffer = Buffer.from(this.receiveBuffer, totalLength);
60
- }
61
- else {
62
- this.receiveBuffer = Buffer.alloc(0);
63
- }
59
+ if (this.receiveBuffer.length < totalLength) {
60
+ break;
61
+ }
62
+ const messageBuffer = this.receiveBuffer.subarray(4, totalLength);
63
+ this.receiveBuffer = this.receiveBuffer.subarray(totalLength);
64
+ // One bad frame can't be tied to a specific request, so fail every
65
+ // in-flight request rather than let any hang forever (invoke has no timeout).
66
+ try {
64
67
  this.onMessage(messageBuffer);
65
68
  }
69
+ catch (err) {
70
+ this.failAllPendingRequests(new Error('Unexpected error handling automation-channel message: ' +
71
+ (err instanceof Error ? err.message : String(err))));
72
+ }
66
73
  }
67
74
  }
68
75
  onEnd() {
@@ -74,36 +81,82 @@ class AutomationClient {
74
81
  onMessage(message) {
75
82
  const response = jsonrpc_lite_1.default.parseJsonRpcString(message.toString('utf8'));
76
83
  if (Array.isArray(response)) {
77
- throw new Error('Expected single message');
84
+ this.failAllPendingRequests(new Error('Unexpected JSON-RPC batch response'));
85
+ return;
78
86
  }
79
87
  switch (response.type) {
88
+ // The server only ever sends responses; a request/notification frame means
89
+ // the stream is out of sync, so fail all in-flight requests rather than
90
+ // silently ignore it (invoke has no timeout).
80
91
  case "request" /* RpcStatusType.request */:
81
- throw new Error('Received JSON-RPC request instead of response');
82
92
  case "notification" /* RpcStatusType.notification */:
83
- throw new Error('Unexpected JSON-RPC notification');
84
- case "invalid" /* RpcStatusType.invalid */:
85
- throw new Error('Invalid JSON-RPC2 response: ' +
86
- JSON.stringify(response.payload, null, 2));
93
+ this.failAllPendingRequests(new Error('Unexpected JSON-RPC message from server'));
94
+ return;
95
+ case "invalid" /* RpcStatusType.invalid */: {
96
+ // jsonrpc-lite keeps the raw response under `data`; use its id to reject
97
+ // the matching request so its caller fails fast instead of hanging.
98
+ const rawId = response.payload?.data?.id;
99
+ this.rejectPendingRequest(rawId, new Error('Invalid JSON-RPC2 response: ' +
100
+ JSON.stringify(response.payload, null, 2)));
101
+ return;
102
+ }
87
103
  case "success" /* RpcStatusType.success */: {
88
104
  const pendingReq = this.pendingRequests.get(response.payload.id);
89
105
  if (!pendingReq) {
90
- throw new Error('Could not find pending request from response ID');
106
+ this.failUnexpectedResponseId(response.payload.id);
107
+ return;
91
108
  }
92
109
  this.pendingRequests.delete(response.payload.id);
93
110
  pendingReq({ type: 'success', result: response.payload.result }, null);
94
- break;
111
+ return;
95
112
  }
96
113
  case "error" /* RpcStatusType.error */: {
97
- const pendingReq = this.pendingRequests.get(response.payload.id);
98
- if (!pendingReq) {
99
- throw new Error('Could not find pending request from response ID');
114
+ const id = response.payload.id;
115
+ const pendingReq = id === null ? undefined : this.pendingRequests.get(id);
116
+ if (pendingReq) {
117
+ this.pendingRequests.delete(id);
118
+ pendingReq({ type: 'error', ...response.payload.error }, null);
100
119
  }
101
- this.pendingRequests.delete(response.payload.id);
102
- pendingReq({ type: 'error', ...response.payload.error }, null);
103
- break;
120
+ else if (id === null) {
121
+ // A null id means the server couldn't attribute the error to a
122
+ // request (e.g. a parse error on our stream); fail all so none hang.
123
+ this.failAllPendingRequests(new Error('JSON-RPC error with null id: ' +
124
+ JSON.stringify(response.payload.error)));
125
+ }
126
+ else {
127
+ this.failUnexpectedResponseId(id);
128
+ }
129
+ return;
104
130
  }
105
131
  }
106
132
  }
133
+ rejectPendingRequest(id, err) {
134
+ // This client only issues numeric ids, so a non-numeric id can't match a
135
+ // pending request; treat it as unattributable and fail all (invoke has no timeout).
136
+ if (typeof id !== 'number') {
137
+ this.failAllPendingRequests(err);
138
+ return;
139
+ }
140
+ const pendingReq = this.pendingRequests.get(id);
141
+ if (pendingReq) {
142
+ this.pendingRequests.delete(id);
143
+ pendingReq(null, err);
144
+ }
145
+ else {
146
+ this.failAllPendingRequests(err);
147
+ }
148
+ }
149
+ failUnexpectedResponseId(id) {
150
+ this.failAllPendingRequests(new Error('Could not find pending request for JSON-RPC response ID ' +
151
+ JSON.stringify(id)));
152
+ }
153
+ failAllPendingRequests(err) {
154
+ const pending = Array.from(this.pendingRequests.values());
155
+ this.pendingRequests.clear();
156
+ for (const req of pending) {
157
+ req(null, err);
158
+ }
159
+ }
107
160
  }
108
161
  exports.AutomationClient = AutomationClient;
109
162
  function waitForConnection(opts) {
@@ -1 +1 @@
1
- {"version":3,"file":"automationChannel.js","sourceRoot":"","sources":["../src/automationChannel.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;AAEH,6BAAmC;AACnC,gEAAmC;AAGnC,IAAI,cAAc,GAAG,CAAC,CAAC;AAMvB,MAAa,gBAAgB;IAS3B,YAAY,MAAc,EAAE,MAAc;QACxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,IAAI,CAAC,eAAe,GAAG,IAAI,GAAG,EAAE,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAErC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,CACJ,UAAkB,EAClB,MAAmC;QAEnC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,SAAS,GAAG,EAAE,cAAc,CAAC;YACnC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;gBAClD,IAAI,GAAG,EAAE;oBACP,MAAM,CAAC,GAAG,CAAC,CAAC;iBACb;gBACD,IAAI,MAAM,EAAE;oBACV,OAAO,CAAC,MAAM,CAAC,CAAC;iBACjB;YACH,CAAC,CAAC,CAAC;YAEH,MAAM,aAAa,GAAG,sBAAO;iBAC1B,OAAO,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC;iBACtC,SAAS,EAAE,CAAC;YAEf,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;YAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACnC,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YAE/C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC9B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAEO,MAAM,CAAC,KAAa;QAC1B,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,IAAI,CAAC,EAAE;YAClC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;YACxD,MAAM,WAAW,GAAG,aAAa,GAAG,CAAC,CAAC;YAEtC,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,IAAI,WAAW,EAAE;gBAC5C,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;gBAE/D,IAAI,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE;oBAC3C,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;iBACnE;qBAAM;oBACL,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;iBACtC;gBAED,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;aAC/B;SACF;IACH,CAAC;IAEO,KAAK;QACX,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CACjC,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC,CAC9D,CAAC;IACJ,CAAC;IAEO,OAAO,CAAC,KAAY;QAC1B,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IACxD,CAAC;IAEO,SAAS,CAAC,OAAe;QAC/B,MAAM,QAAQ,GAAG,sBAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACtE,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;YAC3B,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;SAC5C;QAED,QAAQ,QAAQ,CAAC,IAAI,EAAE;YACrB;gBACE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACnE;gBACE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;YACtD;gBACE,MAAM,IAAI,KAAK,CACb,8BAA8B;oBAC5B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAC5C,CAAC;YAEJ,0CAA0B,CAAC,CAAC;gBAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjE,IAAI,CAAC,UAAU,EAAE;oBACf,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;iBACpE;gBAED,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjD,UAAU,CAAC,EAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAC,EAAE,IAAI,CAAC,CAAC;gBACrE,MAAM;aACP;YAED,sCAAwB,CAAC,CAAC;gBACxB,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjE,IAAI,CAAC,UAAU,EAAE;oBACf,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;iBACpE;gBAED,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjD,UAAU,CAAC,EAAC,IAAI,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAC,EAAE,IAAI,CAAC,CAAC;gBAC7D,MAAM;aACP;SACF;IACH,CAAC;CACF;AA5HD,4CA4HC;AAED,SAAgB,iBAAiB,CAAC,IAEjC;IACC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,IAAI,YAAM,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEzB,MAAM,OAAO,GAAG,CAAC,GAAU,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5B,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE;YAC/B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7B,OAAO,CAAC,IAAI,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAdD,8CAcC","sourcesContent":["/**\n * Copyright (c) Microsoft Corporation.\n * Licensed under the MIT License.\n *\n * @format\n */\n\nimport {Server, Socket} from 'net';\nimport jsonrpc from 'jsonrpc-lite';\nimport {RpcStatusType} from 'jsonrpc-lite';\n\nlet incrementingId = 0;\n\nexport type InvokeResult =\n | {type: 'error'; code: any; message: string}\n | {type: 'success'; result?: any};\n\nexport class AutomationClient {\n private readonly socket: Socket;\n private readonly server: Server;\n private readonly pendingRequests: Map<\n any,\n (result: InvokeResult | null, err: Error | null) => void\n >;\n private receiveBuffer: Buffer;\n\n constructor(socket: Socket, server: Server) {\n this.socket = socket;\n this.server = server;\n\n this.pendingRequests = new Map();\n this.receiveBuffer = Buffer.alloc(0);\n\n this.socket.on('data', this.onData.bind(this));\n this.socket.on('end', this.onEnd.bind(this));\n this.socket.on('error', this.onError.bind(this));\n }\n\n invoke(\n methodName: string,\n params: any[] | Record<string, any>,\n ): Promise<InvokeResult> {\n return new Promise((resolve, reject) => {\n const messageId = ++incrementingId;\n this.pendingRequests.set(messageId, (result, err) => {\n if (err) {\n reject(err);\n }\n if (result) {\n resolve(result);\n }\n });\n\n const requestString = jsonrpc\n .request(messageId, methodName, params)\n .serialize();\n\n const requestBuffer = Buffer.from(requestString, 'utf-8');\n const sizeBuffer = Buffer.alloc(4);\n sizeBuffer.writeUInt32LE(requestBuffer.length);\n\n this.socket.write(sizeBuffer);\n this.socket.write(requestBuffer);\n });\n }\n\n close() {\n this.socket.destroy();\n this.server.close();\n }\n\n private onData(chunk: Buffer) {\n this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);\n if (this.receiveBuffer.length >= 4) {\n const messageLength = this.receiveBuffer.readUInt32LE();\n const totalLength = messageLength + 4;\n\n if (this.receiveBuffer.length >= totalLength) {\n const messageBuffer = this.receiveBuffer.slice(4, totalLength);\n\n if (totalLength < this.receiveBuffer.length) {\n this.receiveBuffer = Buffer.from(this.receiveBuffer, totalLength);\n } else {\n this.receiveBuffer = Buffer.alloc(0);\n }\n\n this.onMessage(messageBuffer);\n }\n }\n }\n\n private onEnd() {\n this.pendingRequests.forEach(req =>\n req(null, new Error('Unexpected disconnect from RPC server')),\n );\n }\n\n private onError(error: Error) {\n this.pendingRequests.forEach(req => req(null, error));\n }\n\n private onMessage(message: Buffer) {\n const response = jsonrpc.parseJsonRpcString(message.toString('utf8'));\n if (Array.isArray(response)) {\n throw new Error('Expected single message');\n }\n\n switch (response.type) {\n case RpcStatusType.request:\n throw new Error('Received JSON-RPC request instead of response');\n case RpcStatusType.notification:\n throw new Error('Unexpected JSON-RPC notification');\n case RpcStatusType.invalid:\n throw new Error(\n 'Invalid JSON-RPC2 response: ' +\n JSON.stringify(response.payload, null, 2),\n );\n\n case RpcStatusType.success: {\n const pendingReq = this.pendingRequests.get(response.payload.id);\n if (!pendingReq) {\n throw new Error('Could not find pending request from response ID');\n }\n\n this.pendingRequests.delete(response.payload.id);\n pendingReq({type: 'success', result: response.payload.result}, null);\n break;\n }\n\n case RpcStatusType.error: {\n const pendingReq = this.pendingRequests.get(response.payload.id);\n if (!pendingReq) {\n throw new Error('Could not find pending request from response ID');\n }\n\n this.pendingRequests.delete(response.payload.id);\n pendingReq({type: 'error', ...response.payload.error}, null);\n break;\n }\n }\n }\n}\n\nexport function waitForConnection(opts: {\n port: number;\n}): Promise<AutomationClient> {\n return new Promise((resolve, reject) => {\n const server = new Server();\n server.listen(opts.port);\n\n const onError = (err: Error) => reject(err);\n server.on('error', onError);\n server.on('connection', socket => {\n server.off('error', onError);\n resolve(new AutomationClient(socket, server));\n });\n });\n}\n"]}
1
+ {"version":3,"file":"automationChannel.js","sourceRoot":"","sources":["../src/automationChannel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Copyright (c) Microsoft Corporation.\n * Licensed under the MIT License.\n *\n * @format\n */\n\nimport {Server, Socket} from 'net';\nimport jsonrpc from 'jsonrpc-lite';\nimport {RpcStatusType} from 'jsonrpc-lite';\n\nlet incrementingId = 0;\n\nexport type InvokeResult =\n | {type: 'error'; code: any; message: string}\n | {type: 'success'; result?: any};\n\nexport class AutomationClient {\n private readonly socket: Socket;\n private readonly server: Server;\n private readonly pendingRequests: Map<\n any,\n (result: InvokeResult | null, err: Error | null) => void\n >;\n private receiveBuffer: Buffer;\n\n constructor(socket: Socket, server: Server) {\n this.socket = socket;\n this.server = server;\n\n this.pendingRequests = new Map();\n this.receiveBuffer = Buffer.alloc(0);\n\n this.socket.on('data', this.onData.bind(this));\n this.socket.on('end', this.onEnd.bind(this));\n this.socket.on('error', this.onError.bind(this));\n }\n\n invoke(\n methodName: string,\n params: any[] | Record<string, any>,\n ): Promise<InvokeResult> {\n return new Promise((resolve, reject) => {\n const messageId = ++incrementingId;\n this.pendingRequests.set(messageId, (result, err) => {\n if (err) {\n reject(err);\n }\n if (result) {\n resolve(result);\n }\n });\n\n const requestString = jsonrpc\n .request(messageId, methodName, params)\n .serialize();\n\n const requestBuffer = Buffer.from(requestString, 'utf-8');\n const sizeBuffer = Buffer.alloc(4);\n sizeBuffer.writeUInt32LE(requestBuffer.length);\n\n this.socket.write(sizeBuffer);\n this.socket.write(requestBuffer);\n });\n }\n\n close() {\n this.socket.destroy();\n this.server.close();\n }\n\n private onData(chunk: Buffer) {\n this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);\n\n // Localhost TCP can coalesce responses into one chunk, so drain every\n // complete frame and advance past it; stopping after one leaves later\n // responses stuck behind it and wedges the channel.\n while (this.receiveBuffer.length >= 4) {\n const messageLength = this.receiveBuffer.readUInt32LE();\n const totalLength = messageLength + 4;\n if (this.receiveBuffer.length < totalLength) {\n break;\n }\n\n const messageBuffer = this.receiveBuffer.subarray(4, totalLength);\n this.receiveBuffer = this.receiveBuffer.subarray(totalLength);\n\n // One bad frame can't be tied to a specific request, so fail every\n // in-flight request rather than let any hang forever (invoke has no timeout).\n try {\n this.onMessage(messageBuffer);\n } catch (err) {\n this.failAllPendingRequests(\n new Error(\n 'Unexpected error handling automation-channel message: ' +\n (err instanceof Error ? err.message : String(err)),\n ),\n );\n }\n }\n }\n\n private onEnd() {\n this.pendingRequests.forEach(req =>\n req(null, new Error('Unexpected disconnect from RPC server')),\n );\n }\n\n private onError(error: Error) {\n this.pendingRequests.forEach(req => req(null, error));\n }\n\n private onMessage(message: Buffer) {\n const response = jsonrpc.parseJsonRpcString(message.toString('utf8'));\n if (Array.isArray(response)) {\n this.failAllPendingRequests(\n new Error('Unexpected JSON-RPC batch response'),\n );\n return;\n }\n\n switch (response.type) {\n // The server only ever sends responses; a request/notification frame means\n // the stream is out of sync, so fail all in-flight requests rather than\n // silently ignore it (invoke has no timeout).\n case RpcStatusType.request:\n case RpcStatusType.notification:\n this.failAllPendingRequests(\n new Error('Unexpected JSON-RPC message from server'),\n );\n return;\n\n case RpcStatusType.invalid: {\n // jsonrpc-lite keeps the raw response under `data`; use its id to reject\n // the matching request so its caller fails fast instead of hanging.\n const rawId = (response.payload as any)?.data?.id;\n this.rejectPendingRequest(\n rawId,\n new Error(\n 'Invalid JSON-RPC2 response: ' +\n JSON.stringify(response.payload, null, 2),\n ),\n );\n return;\n }\n\n case RpcStatusType.success: {\n const pendingReq = this.pendingRequests.get(response.payload.id);\n if (!pendingReq) {\n this.failUnexpectedResponseId(response.payload.id);\n return;\n }\n\n this.pendingRequests.delete(response.payload.id);\n pendingReq({type: 'success', result: response.payload.result}, null);\n return;\n }\n\n case RpcStatusType.error: {\n const id = response.payload.id;\n const pendingReq =\n id === null ? undefined : this.pendingRequests.get(id);\n if (pendingReq) {\n this.pendingRequests.delete(id);\n pendingReq({type: 'error', ...response.payload.error}, null);\n } else if (id === null) {\n // A null id means the server couldn't attribute the error to a\n // request (e.g. a parse error on our stream); fail all so none hang.\n this.failAllPendingRequests(\n new Error(\n 'JSON-RPC error with null id: ' +\n JSON.stringify(response.payload.error),\n ),\n );\n } else {\n this.failUnexpectedResponseId(id);\n }\n return;\n }\n }\n }\n\n private rejectPendingRequest(id: any, err: Error) {\n // This client only issues numeric ids, so a non-numeric id can't match a\n // pending request; treat it as unattributable and fail all (invoke has no timeout).\n if (typeof id !== 'number') {\n this.failAllPendingRequests(err);\n return;\n }\n\n const pendingReq = this.pendingRequests.get(id);\n if (pendingReq) {\n this.pendingRequests.delete(id);\n pendingReq(null, err);\n } else {\n this.failAllPendingRequests(err);\n }\n }\n\n private failUnexpectedResponseId(id: any) {\n this.failAllPendingRequests(\n new Error(\n 'Could not find pending request for JSON-RPC response ID ' +\n JSON.stringify(id),\n ),\n );\n }\n\n private failAllPendingRequests(err: Error) {\n const pending = Array.from(this.pendingRequests.values());\n this.pendingRequests.clear();\n for (const req of pending) {\n req(null, err);\n }\n }\n}\n\nexport function waitForConnection(opts: {\n port: number;\n}): Promise<AutomationClient> {\n return new Promise((resolve, reject) => {\n const server = new Server();\n server.listen(opts.port);\n\n const onError = (err: Error) => reject(err);\n server.on('error', onError);\n server.on('connection', socket => {\n server.off('error', onError);\n resolve(new AutomationClient(socket, server));\n });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-windows/automation-channel",
3
- "version": "0.85.0-preview.1",
3
+ "version": "0.85.0-preview.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "prettier": "^3.6.2",
35
35
  "react": "19.2.3",
36
36
  "react-native": "0.85.3",
37
- "react-native-windows": "0.85.0-preview.1",
37
+ "react-native-windows": "0.85.0-preview.2",
38
38
  "typescript": "5.0.4"
39
39
  },
40
40
  "files": [
@@ -86,6 +86,9 @@ IAsyncOperation<IJsonValue> CommandHandler::Invoke(const winrt::hstring &methodN
86
86
  auto result = co_await asyncOperation->second(params);
87
87
  co_return result;
88
88
  }
89
+
90
+ // Unknown methods surface as JSON-RPC MethodNotFound in the request processor; this guards direct callers.
91
+ throw winrt::hresult_invalid_argument(L"No automation-channel handler registered for method '" + methodName + L"'");
89
92
  }
90
93
 
91
94
  } // namespace winrt::AutomationChannel::implementation
@@ -19,6 +19,8 @@ namespace AutomationChannel {
19
19
  CommandHandler BindOperation(String methodName, SyncOperation operation);
20
20
  CommandHandler BindAsyncOperation(String methodName, AsyncOperation operation);
21
21
 
22
+ Boolean IsMethodRegistered(String methodName);
23
+
22
24
  // May throw on delegate error
23
25
  Windows.Foundation.IAsyncOperation<Windows.Data.Json.IJsonValue> Invoke(String methodName, Windows.Data.Json.JsonValue params);
24
26
  };
@@ -29,7 +29,13 @@ winrt::fire_and_forget JsonRpcRequestProcessor::HandleRequest(
29
29
  winrt::hstring errorMessage;
30
30
 
31
31
  try {
32
- auto result = co_await handler.Invoke(message.GetNamedString(L"method"), message.GetNamedValue(L"params"));
32
+ auto method = message.GetNamedString(L"method");
33
+ if (!handler.IsMethodRegistered(method)) {
34
+ co_await EmitError(
35
+ JsonRpcErrorCode::MethodNotFound, L"Method not found: " + method, message.GetNamedValue(L"id"), output);
36
+ co_return;
37
+ }
38
+ auto result = co_await handler.Invoke(method, message.GetNamedValue(L"params"));
33
39
  co_await EmitResult(result, message.GetNamedValue(L"id"), output);
34
40
  co_return;
35
41
  } catch (const winrt::hresult_error &ex) {
@@ -62,14 +68,23 @@ IAsyncAction JsonRpcRequestProcessor::EmitError(
62
68
  winrt::hstring message,
63
69
  JsonValue id,
64
70
  IOutputStream output) noexcept {
65
- JsonObject err;
66
- err.SetNamedValue(L"code", JsonValue::CreateNumberValue(static_cast<int32_t>(code)));
67
- err.SetNamedValue(L"message", JsonValue::CreateStringValue(message));
68
- co_await EmitResponse(err, id, output);
71
+ JsonObject error;
72
+ error.SetNamedValue(L"code", JsonValue::CreateNumberValue(static_cast<int32_t>(code)));
73
+ error.SetNamedValue(L"message", JsonValue::CreateStringValue(message));
74
+
75
+ // JSON-RPC 2.0 requires code/message under an "error" member; at top level the client rejects it as invalid.
76
+ JsonObject res;
77
+ res.SetNamedValue(L"error", error);
78
+ co_await EmitResponse(res, id, output);
69
79
  }
70
80
 
71
81
  IAsyncAction JsonRpcRequestProcessor::EmitResult(IJsonValue result, JsonValue id, IOutputStream output) noexcept {
72
82
  JsonObject res;
83
+ // A null result must still serialize as "result": null. SetNamedValue with a
84
+ // null value drops the key, producing a response the client rejects as invalid.
85
+ if (!result) {
86
+ result = JsonValue::CreateNullValue();
87
+ }
73
88
  res.SetNamedValue(L"result", result);
74
89
  co_await EmitResponse(res, id, output);
75
90
  }